use crate::TastyTrade;
use crate::api::quote_streaming::AsStreamerSymbol;
use crate::streaming::reconnect::{BackoffPolicy, ConnectionState};
use crate::types::dxfeed;
use crate::types::dxfeed::{CandlePeriod, EventKind};
use crate::{TastyResult, TastyTradeError};
use chrono::{DateTime, Utc};
use dxlink::{DXLinkClient, EventType, FeedSubscription, MarketEvent, OverflowPolicy};
use pretty_simple_display::{DebugPretty, DisplaySimple};
use serde::Serialize;
use std::collections::{BTreeSet, HashMap, HashSet, VecDeque};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::sync::{Notify, RwLock, mpsc, oneshot};
use tracing::{debug, error, info, warn};
#[derive(DebugPretty, DisplaySimple, Serialize, PartialEq, Eq, Hash, Clone, Copy)]
pub struct SubscriptionId(usize);
const DROP_MIRROR_INTERVAL: Duration = Duration::from_millis(100);
const MAX_PENDING_MARKERS: usize = 64;
#[derive(Clone)]
struct StreamerHandle {
commands: Option<mpsc::Sender<DXLinkCommand>>,
}
pub struct QuoteSubscription {
pub id: SubscriptionId,
streamer: StreamerHandle,
kinds: BTreeSet<EventKind>,
event_receiver: flume::Receiver<dxfeed::Event>, dxlink_receiver: mpsc::Receiver<Delivery>, targets: Arc<Mutex<BTreeSet<FeedTarget>>>,
lagged: Arc<AtomicU64>,
progress: CandleProgress,
drained: Arc<Notify>,
history: Arc<Notify>,
closed: Arc<AtomicBool>,
}
impl QuoteSubscription {
pub async fn add_symbols<S: AsStreamerSymbol>(&self, symbols: &[S]) -> TastyResult<()> {
let kinds: Vec<EventKind> = self
.kinds
.iter()
.copied()
.filter(|kind| !kind.needs_a_period())
.collect();
if kinds.is_empty() {
return Err(TastyTradeError::Precondition(
"this subscription asked for candles only, and a candle needs a period and a \
start time; use add_candles"
.to_string(),
));
}
let requested: Vec<FeedTarget> = symbols
.iter()
.flat_map(|symbol| {
let symbol = symbol.as_streamer_symbol();
kinds.iter().map(move |kind| FeedTarget {
kind: *kind,
symbol: symbol.0.clone(),
from_time: None,
})
})
.collect();
self.subscribe_targets(requested).await
}
pub async fn add_candles<S: AsStreamerSymbol>(
&self,
symbols: &[S],
period: CandlePeriod,
from_time: DateTime<Utc>,
) -> TastyResult<()> {
if !self.kinds.contains(&EventKind::Candle) {
return Err(TastyTradeError::Precondition(
"this subscription did not ask for candles, so the channel is not configured to \
deliver them; create one with EventKind::Candle"
.to_string(),
));
}
let from_time = from_time.timestamp_millis();
let requested: Vec<FeedTarget> = symbols
.iter()
.map(|symbol| FeedTarget {
kind: EventKind::Candle,
symbol: period.streamer_symbol(&symbol.as_streamer_symbol().0),
from_time: Some(from_time),
})
.collect();
self.subscribe_targets(requested).await
}
pub fn lagged(&self) -> u64 {
self.lagged.load(Ordering::Relaxed)
}
pub fn subscribed(&self) -> Vec<(String, EventKind)> {
targets_of(&self.targets)
.iter()
.map(|target| (target.symbol.clone(), target.kind))
.collect()
}
async fn subscribe_targets(&self, requested: Vec<FeedTarget>) -> TastyResult<()> {
let targets: Vec<FeedTarget> = {
let mut known = targets_of(&self.targets);
if self.closed.load(Ordering::Acquire) {
return Err(TastyTradeError::Streaming(
"the subscription is closed; create a new one to subscribe".to_string(),
));
}
requested
.into_iter()
.filter(|target| known.insert(target.clone()))
.collect()
};
let subscriptions = feed_subscriptions(&targets);
if subscriptions.is_empty() {
return Ok(());
}
let sub_id = self.id.0 as u32;
let Some(tx) = &self.streamer.commands else {
let mut known = targets_of(&self.targets);
for target in &targets {
known.remove(target);
}
return Err(TastyTradeError::Streaming(
"the quote streamer has no command channel; reconnect before subscribing"
.to_string(),
));
};
let (ack, answered) = oneshot::channel();
let queued = tx
.send(DXLinkCommand::Subscribe(
subscriptions,
targets.iter().map(|target| target.kind).collect(),
sub_id,
self.targets.clone(),
Some(ack),
))
.await
.map_err(|_| {
TastyTradeError::Streaming(
"the quote streamer is closed; reconnect before subscribing".to_string(),
)
});
let outcome = match queued {
Ok(()) => answered.await.unwrap_or_else(|_| {
Err(TastyTradeError::Streaming(
"the quote streamer closed before the subscription was confirmed".to_string(),
))
}),
Err(e) => Err(e),
};
if outcome.is_err() {
let mut known = targets_of(&self.targets);
for target in &targets {
known.remove(target);
}
}
outcome
}
pub async fn get_event(&mut self) -> Result<dxfeed::Event, flume::RecvError> {
loop {
let Some(delivery) = self.dxlink_receiver.recv().await else {
return self.event_receiver.recv_async().await;
};
self.drained.notify_one();
match delivery {
Delivery::Marker(event) => return Ok(event),
Delivery::Market(market_event) => {
if let Some(event) = convert_event(market_event) {
return Ok(event);
}
}
}
}
}
pub fn history_loaded(&self, symbol: &str) -> bool {
self.finished_history(symbol).is_some()
}
fn finished_history(&self, symbol: &str) -> Option<dxfeed::DxfSnapshotEndT> {
let seen = self
.progress
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let resume = seen.get(&(self.id.0 as u32, symbol.to_string()))?;
if resume.phase != SnapshotPhase::Ended {
return None;
}
Some(dxfeed::DxfSnapshotEndT {
generation: resume.generation,
kind: resume.ended_as?,
lossless: resume.lossless,
})
}
pub async fn await_history(&self, symbol: &str) -> TastyResult<dxfeed::DxfSnapshotEndT> {
loop {
let notified = self.history.notified();
tokio::pin!(notified);
notified.as_mut().enable();
if !self.holds_candle_series(symbol) {
return Err(TastyTradeError::Precondition(format!(
"this subscription has no candle series for {symbol}, so its history can \
never finish; subscribe with add_candles first, and pass the streamer \
symbol with its period suffix"
)));
}
if let Some(end) = self.finished_history(symbol) {
return Ok(end);
}
notified.await;
}
}
fn holds_candle_series(&self, symbol: &str) -> bool {
targets_of(&self.targets)
.iter()
.any(|target| target.kind == EventKind::Candle && target.symbol == symbol)
}
pub async fn remove_candles<S: AsStreamerSymbol>(
&self,
symbols: &[S],
period: CandlePeriod,
) -> TastyResult<()> {
let wanted: Vec<String> = symbols
.iter()
.map(|symbol| period.streamer_symbol(&symbol.as_streamer_symbol().0))
.collect();
self.unsubscribe_targets(&wanted).await
}
async fn unsubscribe_targets(&self, symbols: &[String]) -> TastyResult<()> {
let targets: Vec<FeedTarget> = {
let known = targets_of(&self.targets);
known
.iter()
.filter(|target| {
target.kind == EventKind::Candle && symbols.contains(&target.symbol)
})
.cloned()
.collect()
};
if targets.is_empty() {
return Ok(());
}
let sub_id = self.id.0 as u32;
let Some(tx) = &self.streamer.commands else {
return Err(TastyTradeError::Streaming(
"the quote streamer has no command channel; reconnect before unsubscribing"
.to_string(),
));
};
{
let mut known = targets_of(&self.targets);
for target in &targets {
known.remove(target);
}
}
let pending = RemovedTargets {
targets: &self.targets,
removed: targets,
closing: None,
};
let (ack, answered) = oneshot::channel();
let queued = tx
.send(DXLinkCommand::Unsubscribe(
feed_subscriptions(&pending.removed),
sub_id,
Some(self.targets.clone()),
Some(ack),
))
.await
.map_err(|_| {
TastyTradeError::Streaming(
"the quote streamer is closed; reconnect before unsubscribing".to_string(),
)
});
let outcome = match queued {
Ok(()) => answered.await.unwrap_or_else(|_| {
Err(TastyTradeError::Streaming(
"the quote streamer closed before the unsubscribe was confirmed".to_string(),
))
}),
Err(e) => Err(e),
};
outcome?;
let targets = pending.commit();
{
let known = targets_of(&self.targets);
let mut seen = self
.progress
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
for target in &targets {
let still_held = known
.iter()
.any(|held| held.kind == target.kind && held.symbol == target.symbol);
if still_held {
continue;
}
if let Some(resume) = seen.get_mut(&(sub_id, target.symbol.clone())) {
*resume = CandleResume {
generation: resume.generation,
dxlink_drops_at_start: resume.dxlink_drops_at_start,
..CandleResume::new(resume.dxlink_drops_at_start)
};
}
}
}
self.history.notify_waiters();
Ok(())
}
}
fn convert_event(event: MarketEvent) -> Option<dxfeed::Event> {
let data = match event {
MarketEvent::Quote(quote) => {
return Some(dxfeed::Event {
sym: quote.event_symbol,
data: dxfeed::EventData::Quote(dxfeed::DxfQuoteT {
time: 0,
sequence: 0,
time_nanos: 0,
bid_time: 0,
bid_exchange_code: 0,
bid_price: quote.bid_price,
ask_price: quote.ask_price,
bid_size: quote.bid_size as i64,
ask_time: 0,
ask_size: quote.ask_size as i64,
ask_exchange_code: 0,
scope: 0,
}),
});
}
MarketEvent::Trade(trade) => {
return Some(dxfeed::Event {
sym: trade.event_symbol,
data: dxfeed::EventData::Trade(dxfeed::DxfTradeT {
time: 0,
sequence: 0,
time_nanos: 0,
exchange_code: 0,
price: trade.price,
size: trade.size as i64,
tick: 0,
change: 0.0,
day_id: 0,
day_volume: trade.day_volume,
day_turnover: 0.0,
raw_flags: 0,
direction: 0,
is_eth: 0,
scope: 0,
}),
});
}
MarketEvent::Greeks(greeks) => {
return Some(dxfeed::Event {
sym: greeks.event_symbol,
data: dxfeed::EventData::Greeks(dxfeed::DxfGreeksT {
event_flags: 0,
index: 0,
time: 0,
price: 0.0,
volatility: greeks.volatility,
delta: greeks.delta,
gamma: greeks.gamma,
theta: greeks.theta,
vega: greeks.vega,
rho: greeks.rho,
}),
});
}
MarketEvent::TradeETH(trade) => (
trade.event_symbol.clone(),
dxfeed::EventData::TradeEth(Box::new(dxfeed::DxfTradeEthT {
event_time: trade.event_time,
time: trade.time,
time_nano_part: trade.time_nano_part,
sequence: trade.sequence,
exchange_code: trade.exchange_code,
price: trade.price,
change: trade.change,
size: trade.size,
day_id: trade.day_id,
day_volume: trade.day_volume,
day_turnover: trade.day_turnover,
tick_direction: trade.tick_direction,
extended_trading_hours: trade.extended_trading_hours,
})),
),
MarketEvent::Candle(candle) => (
candle.event_symbol.clone(),
dxfeed::EventData::Candle(Box::new(dxfeed::DxfCandleT {
event_time: candle.event_time,
event_flags: candle.event_flags,
index: candle.index,
time: candle.time,
sequence: candle.sequence,
count: candle.count,
open: candle.open,
high: candle.high,
low: candle.low,
close: candle.close,
volume: candle.volume,
vwap: candle.vwap,
bid_volume: candle.bid_volume,
ask_volume: candle.ask_volume,
imp_volatility: candle.imp_volatility,
open_interest: candle.open_interest,
})),
),
MarketEvent::Summary(summary) => (
summary.event_symbol.clone(),
dxfeed::EventData::Summary(Box::new(dxfeed::DxfSummaryT {
event_time: summary.event_time,
day_id: summary.day_id,
day_open_price: summary.day_open_price,
day_high_price: summary.day_high_price,
day_low_price: summary.day_low_price,
day_close_price: summary.day_close_price,
day_close_price_type: summary.day_close_price_type,
prev_day_id: summary.prev_day_id,
prev_day_close_price: summary.prev_day_close_price,
prev_day_close_price_type: summary.prev_day_close_price_type,
prev_day_volume: summary.prev_day_volume,
open_interest: summary.open_interest,
})),
),
MarketEvent::TimeAndSale(sale) => (
sale.event_symbol.clone(),
dxfeed::EventData::TimeAndSale(Box::new(dxfeed::DxfTimeAndSaleT {
event_time: sale.event_time,
event_flags: sale.event_flags,
index: sale.index,
time: sale.time,
time_nano_part: sale.time_nano_part,
sequence: sale.sequence,
exchange_code: sale.exchange_code,
price: sale.price,
size: sale.size,
bid_price: sale.bid_price,
ask_price: sale.ask_price,
exchange_sale_conditions: sale.exchange_sale_conditions,
trade_through_exempt: sale.trade_through_exempt,
aggressor_side: sale.aggressor_side,
spread_leg: sale.spread_leg,
extended_trading_hours: sale.extended_trading_hours,
valid_tick: sale.valid_tick,
sale_type: sale.sale_type,
buyer: sale.buyer,
seller: sale.seller,
})),
),
MarketEvent::Profile(profile) => (
profile.event_symbol.clone(),
dxfeed::EventData::Profile(Box::new(dxfeed::DxfProfileT {
event_time: profile.event_time,
description: profile.description,
short_sale_restriction: profile.short_sale_restriction,
trading_status: profile.trading_status,
status_reason: profile.status_reason,
halt_start_time: profile.halt_start_time,
halt_end_time: profile.halt_end_time,
high_limit_price: profile.high_limit_price,
low_limit_price: profile.low_limit_price,
high_52_week_price: profile.high_52_week_price,
low_52_week_price: profile.low_52_week_price,
beta: profile.beta,
earnings_per_share: profile.earnings_per_share,
dividend_frequency: profile.dividend_frequency,
ex_dividend_amount: profile.ex_dividend_amount,
ex_dividend_day_id: profile.ex_dividend_day_id,
shares: profile.shares,
free_float: profile.free_float,
})),
),
MarketEvent::Underlying(underlying) => (
underlying.event_symbol.clone(),
dxfeed::EventData::Underlying(Box::new(dxfeed::DxfUnderlyingT {
event_time: underlying.event_time,
event_flags: underlying.event_flags,
index: underlying.index,
time: underlying.time,
sequence: underlying.sequence,
volatility: underlying.volatility,
front_volatility: underlying.front_volatility,
back_volatility: underlying.back_volatility,
call_volume: underlying.call_volume,
put_volume: underlying.put_volume,
put_call_ratio: underlying.put_call_ratio,
})),
),
MarketEvent::TheoPrice(theo) => (
theo.event_symbol.clone(),
dxfeed::EventData::TheoPrice(Box::new(dxfeed::DxfTheoPriceT {
event_time: theo.event_time,
event_flags: theo.event_flags,
index: theo.index,
time: theo.time,
sequence: theo.sequence,
price: theo.price,
underlying_price: theo.underlying_price,
delta: theo.delta,
gamma: theo.gamma,
dividend: theo.dividend,
interest: theo.interest,
})),
),
MarketEvent::Series(series) => (
series.event_symbol.clone(),
dxfeed::EventData::Series(Box::new(dxfeed::DxfSeriesT {
event_time: series.event_time,
event_flags: series.event_flags,
index: series.index,
time: series.time,
sequence: series.sequence,
expiration: series.expiration,
volatility: series.volatility,
call_volume: series.call_volume,
put_volume: series.put_volume,
put_call_ratio: series.put_call_ratio,
forward_price: series.forward_price,
dividend: series.dividend,
interest: series.interest,
})),
),
};
Some(dxfeed::Event {
sym: data.0,
data: data.1,
})
}
fn answer(ack: Option<oneshot::Sender<TastyResult<()>>>, outcome: TastyResult<()>) {
if let Some(ack) = ack {
let _ = ack.send(outcome);
}
}
pub const DEFAULT_EVENT_CHANNEL_CAPACITY: usize = 4096;
#[derive(Clone)]
struct Subscriber {
events: mpsc::Sender<Delivery>,
lagged: Arc<AtomicU64>,
pending: Arc<Mutex<VecDeque<Delivery>>>,
}
#[derive(Debug, Clone)]
enum Delivery {
Market(MarketEvent),
Marker(dxfeed::Event),
}
mod snapshot_flags {
pub const TX_PENDING: i64 = 0x01;
pub const REMOVE_EVENT: i64 = 0x02;
pub const SNAPSHOT_BEGIN: i64 = 0x04;
pub const SNAPSHOT_END: i64 = 0x08;
pub const SNAPSHOT_SNIP: i64 = 0x10;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct SnapshotSignals {
begin: bool,
end: Option<dxfeed::SnapshotEndKind>,
tx_pending: bool,
data_less: bool,
}
impl SnapshotSignals {
fn of(flags: i64) -> Self {
let end = if flags & snapshot_flags::SNAPSHOT_SNIP != 0 {
Some(dxfeed::SnapshotEndKind::Snip)
} else if flags & snapshot_flags::SNAPSHOT_END != 0 {
Some(dxfeed::SnapshotEndKind::End)
} else {
None
};
Self {
begin: flags & snapshot_flags::SNAPSHOT_BEGIN != 0,
end,
tx_pending: flags & snapshot_flags::TX_PENDING != 0,
data_less: end.is_some() && flags & snapshot_flags::REMOVE_EVENT != 0,
}
}
}
fn marker_event(symbol: &str, data: dxfeed::EventData) -> dxfeed::Event {
dxfeed::Event {
sym: symbol.to_string(),
data,
}
}
fn offer(subscriber: &Subscriber, delivery: Delivery) -> bool {
let mut pending = subscriber
.pending
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let room = flush_into(subscriber, &mut pending);
let marker = matches!(delivery, Delivery::Marker(_));
if !room {
if marker {
park(&mut pending, delivery);
return true;
}
return false;
}
match subscriber.events.try_send(delivery) {
Ok(()) => true,
Err(mpsc::error::TrySendError::Full(delivery)) => {
if marker {
park(&mut pending, delivery);
true
} else {
false
}
}
Err(mpsc::error::TrySendError::Closed(_)) => false,
}
}
fn park(pending: &mut VecDeque<Delivery>, delivery: Delivery) {
while pending.len() >= MAX_PENDING_MARKERS {
pending.pop_front();
}
pending.push_back(delivery);
}
fn flush_into(subscriber: &Subscriber, pending: &mut VecDeque<Delivery>) -> bool {
while let Some(front) = pending.pop_front() {
match subscriber.events.try_send(front) {
Ok(()) => {}
Err(mpsc::error::TrySendError::Full(returned)) => {
pending.push_front(returned);
return false;
}
Err(mpsc::error::TrySendError::Closed(_)) => {
pending.clear();
return true;
}
}
}
true
}
async fn flush_pending(routing: &Arc<RwLock<EventRouting>>) {
let routing = routing.read().await;
for subscribers in routing.senders.values() {
for subscriber in subscribers {
let mut pending = subscriber
.pending
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if pending.is_empty() {
continue;
}
flush_into(subscriber, &mut pending);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct CandleResume {
through: Option<i64>,
gap: bool,
generation: u64,
phase: SnapshotPhase,
lossless: bool,
dxlink_drops_at_start: u64,
pending_end: Option<(u64, dxfeed::SnapshotEndKind)>,
ended_as: Option<dxfeed::SnapshotEndKind>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SnapshotPhase {
Idle,
Open,
Ended,
}
impl CandleResume {
fn new(dxlink_drops: u64) -> Self {
Self {
through: None,
gap: false,
generation: 0,
phase: SnapshotPhase::Idle,
lossless: true,
dxlink_drops_at_start: dxlink_drops,
pending_end: None,
ended_as: None,
}
}
}
fn series_entry<'a>(
seen: &'a mut HashMap<(u32, String), CandleResume>,
sub_id: u32,
symbol: &str,
dxlink_drops: &Arc<AtomicU64>,
) -> &'a mut CandleResume {
seen.entry((sub_id, symbol.to_string()))
.or_insert_with(|| CandleResume::new(dxlink_drops.load(Ordering::Relaxed)))
}
fn open_generation(
progress: &CandleProgress,
sub_id: u32,
symbol: &str,
dxlink_drops: &Arc<AtomicU64>,
) -> Option<dxfeed::DxfSnapshotBeginT> {
let mut seen = progress.lock().unwrap_or_else(|p| p.into_inner());
let resume = series_entry(&mut seen, sub_id, symbol, dxlink_drops);
resume.pending_end = None;
if resume.phase == SnapshotPhase::Open {
return None;
}
resume.generation += 1;
resume.phase = SnapshotPhase::Open;
resume.lossless = true;
resume.pending_end = None;
resume.ended_as = None;
resume.dxlink_drops_at_start = dxlink_drops.load(Ordering::Relaxed);
Some(dxfeed::DxfSnapshotBeginT {
generation: resume.generation,
})
}
fn mark_lossy(progress: &CandleProgress, sub_id: u32, symbol: &str, dxlink_drops: &Arc<AtomicU64>) {
let mut seen = progress.lock().unwrap_or_else(|p| p.into_inner());
let resume = series_entry(&mut seen, sub_id, symbol, dxlink_drops);
if resume.phase != SnapshotPhase::Ended {
resume.lossless = false;
}
}
fn arm_end(
progress: &CandleProgress,
sub_id: u32,
symbol: &str,
kind: dxfeed::SnapshotEndKind,
dxlink_drops: &Arc<AtomicU64>,
) {
let mut seen = progress.lock().unwrap_or_else(|p| p.into_inner());
let resume = series_entry(&mut seen, sub_id, symbol, dxlink_drops);
if resume.phase == SnapshotPhase::Ended {
return;
}
resume.pending_end = Some((resume.generation, kind));
}
fn armed_end(
progress: &CandleProgress,
sub_id: u32,
symbol: &str,
dxlink_drops: &Arc<AtomicU64>,
) -> Option<dxfeed::SnapshotEndKind> {
let mut seen = progress.lock().unwrap_or_else(|p| p.into_inner());
let resume = series_entry(&mut seen, sub_id, symbol, dxlink_drops);
let (generation, kind) = resume.pending_end?;
if generation != resume.generation {
resume.pending_end = None;
return None;
}
Some(kind)
}
fn finish_generation(
progress: &CandleProgress,
sub_id: u32,
symbol: &str,
kind: dxfeed::SnapshotEndKind,
dxlink_drops: &Arc<AtomicU64>,
) -> Option<dxfeed::DxfSnapshotEndT> {
let mut seen = progress.lock().unwrap_or_else(|p| p.into_inner());
let resume = series_entry(&mut seen, sub_id, symbol, dxlink_drops);
if resume.phase == SnapshotPhase::Ended {
return None;
}
if resume.phase == SnapshotPhase::Idle {
resume.generation += 1;
}
let shed = dxlink_drops.load(Ordering::Relaxed) > resume.dxlink_drops_at_start;
let lossless = resume.lossless && !shed;
resume.phase = SnapshotPhase::Ended;
resume.pending_end = None;
resume.ended_as = Some(kind);
resume.lossless = lossless;
Some(dxfeed::DxfSnapshotEndT {
generation: resume.generation,
kind,
lossless,
})
}
async fn open_generations_for_the_next_connection(
progress: &CandleProgress,
routing: &Arc<RwLock<EventRouting>>,
history: &Arc<Notify>,
dxlink_drops: &Arc<AtomicU64>,
) {
dxlink_drops.store(0, Ordering::Relaxed);
invalidate_history(progress, routing, history, dxlink_drops).await;
}
async fn invalidate_history(
progress: &CandleProgress,
routing: &Arc<RwLock<EventRouting>>,
history: &Arc<Notify>,
dxlink_drops: &Arc<AtomicU64>,
) {
let opened: Vec<(u32, String, u64)> = {
let mut seen = progress.lock().unwrap_or_else(|p| p.into_inner());
let drops = dxlink_drops.load(Ordering::Relaxed);
seen.iter_mut()
.filter_map(|((sub_id, symbol), resume)| {
if resume.phase == SnapshotPhase::Idle && resume.pending_end.is_none() {
return None;
}
resume.generation += 1;
resume.phase = SnapshotPhase::Open;
resume.lossless = true;
resume.pending_end = None;
resume.ended_as = None;
resume.dxlink_drops_at_start = drops;
Some((*sub_id, symbol.clone(), resume.generation))
})
.collect()
};
if opened.is_empty() {
return;
}
{
let routing = routing.read().await;
for (sub_id, symbol, generation) in &opened {
let Some(subscribers) = routing.senders.get(sub_id) else {
continue;
};
let marker = marker_event(
symbol,
dxfeed::EventData::SnapshotBegin(dxfeed::DxfSnapshotBeginT {
generation: *generation,
}),
);
for subscriber in subscribers {
offer(subscriber, Delivery::Marker(marker.clone()));
}
}
}
history.notify_waiters();
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
struct FeedTarget {
kind: EventKind,
symbol: String,
from_time: Option<i64>,
}
fn feed_subscriptions(targets: &[FeedTarget]) -> Vec<FeedSubscription> {
targets
.iter()
.map(|target| FeedSubscription {
event_type: target.kind.wire_name().to_string(),
symbol: target.symbol.clone(),
from_time: target.from_time,
source: None,
})
.collect()
}
struct RemovedTargets<'a> {
targets: &'a Arc<Mutex<BTreeSet<FeedTarget>>>,
removed: Vec<FeedTarget>,
closing: Option<&'a AtomicBool>,
}
impl RemovedTargets<'_> {
fn commit(mut self) -> Vec<FeedTarget> {
self.closing = None;
std::mem::take(&mut self.removed)
}
}
impl Drop for RemovedTargets<'_> {
fn drop(&mut self) {
let mut known = targets_of(self.targets);
for target in self.removed.drain(..) {
known.insert(target);
}
if let Some(closing) = self.closing {
closing.store(false, Ordering::Release);
}
}
}
fn targets_of(
set: &Mutex<BTreeSet<FeedTarget>>,
) -> std::sync::MutexGuard<'_, BTreeSet<FeedTarget>> {
set.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
}
enum DXLinkCommand {
Subscribe(
Vec<FeedSubscription>,
BTreeSet<EventKind>,
u32,
Arc<Mutex<BTreeSet<FeedTarget>>>,
Option<oneshot::Sender<TastyResult<()>>>,
),
Unsubscribe(
Vec<FeedSubscription>,
u32,
Option<Arc<Mutex<BTreeSet<FeedTarget>>>>,
Option<oneshot::Sender<TastyResult<()>>>,
),
AddEventSender(u32, Subscriber),
RemoveEventSender(u32),
}
#[derive(Default)]
struct EventRouting {
senders: HashMap<u32, Vec<Subscriber>>,
routes: HashMap<(String, EventKind), HashSet<u32>>,
}
type CandleProgress = Arc<Mutex<HashMap<(u32, String), CandleResume>>>;
pub struct QuoteStreamer {
shutdown: Option<oneshot::Sender<()>>,
next_sub_id: usize,
subscription_map: HashMap<SubscriptionId, QuoteSubscription>,
dxlink_command_tx: Option<mpsc::Sender<DXLinkCommand>>,
registry: Registry,
state: Arc<RwLock<ConnectionState>>,
progress: CandleProgress,
drained: Arc<Notify>,
history: Arc<Notify>,
}
#[derive(Clone)]
struct SubscriptionRecord {
kinds: BTreeSet<EventKind>,
targets: Arc<Mutex<BTreeSet<FeedTarget>>>,
}
type Registry = Arc<Mutex<HashMap<u32, SubscriptionRecord>>>;
impl QuoteStreamer {
pub async fn connect(tasty: &TastyTrade) -> TastyResult<Self> {
Self::connect_with_policy(tasty, BackoffPolicy::default()).await
}
pub async fn connect_with_policy(
tasty: &TastyTrade,
policy: BackoffPolicy,
) -> TastyResult<Self> {
let connection = connect_dxlink(tasty).await?;
let (command_tx, command_rx) = mpsc::channel::<DXLinkCommand>(100);
let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
let routing: Arc<RwLock<EventRouting>> = Arc::new(RwLock::new(EventRouting::default()));
let registry: Registry = Arc::new(Mutex::new(HashMap::new()));
let state = Arc::new(RwLock::new(ConnectionState::Connected));
let progress: CandleProgress = Arc::new(Mutex::new(HashMap::new()));
let drained = Arc::new(Notify::new());
let history = Arc::new(Notify::new());
tokio::spawn(supervise(
tasty.clone(),
policy,
connection,
command_rx,
shutdown_rx,
routing,
registry.clone(),
state.clone(),
progress.clone(),
drained.clone(),
history.clone(),
));
Ok(Self {
shutdown: Some(shutdown_tx),
next_sub_id: 0,
subscription_map: HashMap::new(),
dxlink_command_tx: Some(command_tx),
registry,
state,
progress,
drained,
history,
})
}
pub async fn state(&self) -> ConnectionState {
self.state.read().await.clone()
}
fn handle(&self) -> StreamerHandle {
StreamerHandle {
commands: self.dxlink_command_tx.clone(),
}
}
pub async fn create_sub(
&mut self,
kinds: impl IntoIterator<Item = EventKind>,
) -> TastyResult<Box<QuoteSubscription>> {
self.create_sub_with_capacity(kinds, DEFAULT_EVENT_CHANNEL_CAPACITY)
.await
}
pub async fn create_sub_with_capacity(
&mut self,
kinds: impl IntoIterator<Item = EventKind>,
capacity: usize,
) -> TastyResult<Box<QuoteSubscription>> {
if capacity == 0 {
return Err(TastyTradeError::Precondition(
"a subscription with no buffer cannot deliver anything; it would drop every \
event and report itself as lagging"
.to_string(),
));
}
let kinds: BTreeSet<EventKind> = kinds.into_iter().collect();
let id = SubscriptionId(self.next_sub_id);
self.next_sub_id += 1;
let sub_id = id.0 as u32;
let (caller_tx, caller_rx) = mpsc::channel(capacity);
let lagged = Arc::new(AtomicU64::new(0));
let (_event_sender, event_receiver) = flume::unbounded();
let Some(commands) = &self.dxlink_command_tx else {
return Err(TastyTradeError::Streaming(
"the quote streamer has no command channel; reconnect before subscribing"
.to_string(),
));
};
commands
.send(DXLinkCommand::AddEventSender(
sub_id,
Subscriber {
events: caller_tx,
lagged: lagged.clone(),
pending: Arc::new(Mutex::new(VecDeque::new())),
},
))
.await
.map_err(|_| {
TastyTradeError::Streaming(
"the quote streamer is closed; it cannot route events to a new subscription"
.to_string(),
)
})?;
let targets = Arc::new(Mutex::new(BTreeSet::new()));
self.registry
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.insert(
sub_id,
SubscriptionRecord {
kinds: kinds.clone(),
targets: targets.clone(),
},
);
let (_closed, closed_rx) = mpsc::channel(1);
let closed = Arc::new(AtomicBool::new(false));
self.subscription_map.insert(
id,
QuoteSubscription {
id,
streamer: self.handle(),
kinds: kinds.clone(),
event_receiver: event_receiver.clone(),
dxlink_receiver: closed_rx,
targets: targets.clone(),
lagged: lagged.clone(),
progress: self.progress.clone(),
drained: self.drained.clone(),
history: self.history.clone(),
closed: closed.clone(),
},
);
Ok(Box::new(QuoteSubscription {
id,
streamer: self.handle(),
kinds,
event_receiver,
dxlink_receiver: caller_rx,
targets,
lagged,
progress: self.progress.clone(),
drained: self.drained.clone(),
history: self.history.clone(),
closed,
}))
}
pub fn get_sub(&self, id: SubscriptionId) -> Option<&QuoteSubscription> {
self.subscription_map.get(&id)
}
pub async fn close_sub(&mut self, id: SubscriptionId) -> TastyResult<()> {
if let Some(subscription) = self.subscription_map.get(&id) {
let pending = {
let mut known = targets_of(&subscription.targets);
subscription.closed.store(true, Ordering::Release);
let removed: Vec<FeedTarget> = known.iter().cloned().collect();
known.clear();
RemovedTargets {
targets: &subscription.targets,
removed,
closing: Some(&subscription.closed),
}
};
let unsubscribe_requests = feed_subscriptions(&pending.removed);
if let Some(tx) = &self.dxlink_command_tx {
let sub_id = id.0 as u32;
let closed = |_| {
TastyTradeError::Streaming(
"the quote streamer is closed; the subscription is gone with it"
.to_string(),
)
};
if !unsubscribe_requests.is_empty() {
let (ack, answered) = oneshot::channel();
tx.send(DXLinkCommand::Unsubscribe(
unsubscribe_requests,
sub_id,
None,
Some(ack),
))
.await
.map_err(closed)?;
answered.await.unwrap_or_else(|_| {
Err(TastyTradeError::Streaming(
"the quote streamer closed before the unsubscribe was confirmed"
.to_string(),
))
})?;
}
tx.send(DXLinkCommand::RemoveEventSender(sub_id))
.await
.map_err(closed)?;
}
pending.commit();
}
self.subscription_map.remove(&id);
self.registry
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.remove(&(id.0 as u32));
self.progress
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.retain(|(sub_id, _), _| *sub_id != id.0 as u32);
self.history.notify_waiters();
Ok(())
}
}
struct LiveConnection {
client: DXLinkClient,
channel_id: u32,
events: mpsc::Receiver<MarketEvent>,
}
async fn connect_dxlink(tasty: &TastyTrade) -> TastyResult<LiveConnection> {
let tokens = tasty.quote_streamer_tokens().await?;
debug!(
"Obtained DXLink streamer token ({} bytes)",
tokens.token.len()
);
let mut client = DXLinkClient::new(&tokens.streamer_url, &tokens.token)
.with_overflow_policy(OverflowPolicy::Block);
info!("Connecting to DXLink server: {}", tokens.streamer_url);
let events = client.connect().await.map_err(|e| {
let error: TastyTradeError = e.into();
error
})?;
let channel_id = client
.create_feed_channel("AUTO")
.await
.map_err(TastyTradeError::from)?;
info!("DXLink channel created: {}", channel_id);
Ok(LiveConnection {
client,
channel_id,
events,
})
}
async fn ensure_configured(
client: &mut DXLinkClient,
channel_id: u32,
configured: &mut BTreeSet<EventKind>,
wanted: &BTreeSet<EventKind>,
) -> TastyResult<()> {
if wanted.is_subset(configured) {
return Ok(());
}
let union: BTreeSet<EventKind> = configured.union(wanted).copied().collect();
let types: Vec<EventType> = union.iter().copied().map(feed_event_type).collect();
debug!(
"Configuring feed channel {channel_id} for {} event type(s)",
types.len()
);
match client.setup_feed(channel_id, &types).await {
Ok(()) => {
*configured = union;
Ok(())
}
Err(e) => {
let lost = is_connection_lost(&e);
let message = format!("the venue refused the feed configuration: {e}");
Err(if lost {
TastyTradeError::Connection(message)
} else {
TastyTradeError::Streaming(message)
})
}
}
}
enum Ended {
Owner,
ConnectionLost,
}
async fn forward_events(
mut events: mpsc::Receiver<MarketEvent>,
routing: Arc<RwLock<EventRouting>>,
progress: CandleProgress,
saw_event: Arc<AtomicBool>,
drained: Arc<Notify>,
dxlink_drops: Arc<AtomicU64>,
history: Arc<Notify>,
) {
flush_pending(&routing).await;
loop {
let event = tokio::select! {
biased;
() = drained.notified() => {
flush_pending(&routing).await;
continue;
}
event = events.recv() => match event {
Some(event) => event,
None => return,
},
};
saw_event.store(true, Ordering::Relaxed);
let Some(symbol) = event_symbol(&event) else {
continue;
};
let symbol = symbol.to_string();
let kind = event_kind(&event);
let snapshot = match &event {
MarketEvent::Candle(candle) => SnapshotSignals::of(candle.event_flags),
_ => SnapshotSignals::of(0),
};
let routes = routing.read().await;
let Some(sub_ids) = routes.routes.get(&(symbol.clone(), kind)) else {
debug!("No subscription registered for {kind} on {symbol}");
continue;
};
let mut phase_changed = false;
for sub_id in sub_ids {
let Some(subscribers) = routes.senders.get(sub_id) else {
continue;
};
if snapshot.begin
&& let Some(begin) = open_generation(&progress, *sub_id, &symbol, &dxlink_drops)
{
phase_changed = true;
let marker = marker_event(&symbol, dxfeed::EventData::SnapshotBegin(begin));
for subscriber in subscribers {
offer(subscriber, Delivery::Marker(marker.clone()));
}
}
if !snapshot.data_less {
let mut delivered = false;
let mut dropped = 0usize;
for subscriber in subscribers {
if offer(subscriber, Delivery::Market(event.clone())) {
delivered = true;
} else {
subscriber.lagged.fetch_add(1, Ordering::Relaxed);
dropped += 1;
}
}
if dropped > 0 {
warn!(
"A consumer fell behind: dropped {kind} for {symbol} on {dropped} \
channel(s) of subscription {sub_id}"
);
}
if let MarketEvent::Candle(candle) = &event {
record_bar(
&progress,
*sub_id,
&symbol,
candle.time,
delivered && dropped == 0,
&dxlink_drops,
);
if dropped > 0 {
mark_lossy(&progress, *sub_id, &symbol, &dxlink_drops);
}
}
}
if kind == EventKind::Candle {
let finished = match snapshot.end {
Some(end) if snapshot.tx_pending => {
arm_end(&progress, *sub_id, &symbol, end, &dxlink_drops);
None
}
Some(end) => finish_generation(&progress, *sub_id, &symbol, end, &dxlink_drops),
None if !snapshot.tx_pending => {
match armed_end(&progress, *sub_id, &symbol, &dxlink_drops) {
Some(end) => {
finish_generation(&progress, *sub_id, &symbol, end, &dxlink_drops)
}
None => None,
}
}
None => None,
};
if let Some(end) = finished {
phase_changed = true;
let marker = marker_event(&symbol, dxfeed::EventData::SnapshotEnd(end));
for subscriber in subscribers {
offer(subscriber, Delivery::Marker(marker.clone()));
}
}
}
}
drop(routes);
if phase_changed {
history.notify_waiters();
}
}
}
fn record_bar(
progress: &CandleProgress,
sub_id: u32,
symbol: &str,
time: i64,
complete: bool,
dxlink_drops: &Arc<AtomicU64>,
) {
let mut seen = progress.lock().unwrap_or_else(|p| p.into_inner());
let resume = series_entry(&mut seen, sub_id, symbol, dxlink_drops);
if !complete {
resume.gap = true;
return;
}
if !resume.gap {
resume.through = Some(match resume.through {
Some(through) => through.max(time),
None => time,
});
}
}
async fn replay(
client: &mut DXLinkClient,
channel_id: u32,
registry: &Registry,
progress: &CandleProgress,
configured: &mut BTreeSet<EventKind>,
) -> bool {
let pending = pending_replay(registry, progress);
if pending.is_empty() {
return true;
}
let wanted: BTreeSet<EventKind> = registry
.lock()
.unwrap_or_else(|p| p.into_inner())
.values()
.flat_map(|record| record.kinds.iter().copied())
.collect();
if let Err(e) = ensure_configured(client, channel_id, configured, &wanted).await {
warn!("Could not configure the feed channel after reconnecting: {e}");
return false;
}
debug!(
"Restoring {} subscription(s) after a reconnect",
pending.len()
);
{
let mut seen = progress.lock().unwrap_or_else(|p| p.into_inner());
for resume in seen.values_mut() {
resume.gap = false;
}
}
for (sub_id, requests) in pending {
if let Err(e) = client.subscribe(channel_id, requests).await {
warn!("Could not restore subscription {sub_id}: {e}");
return false;
}
}
true
}
fn pending_replay(
registry: &Registry,
progress: &CandleProgress,
) -> Vec<(u32, Vec<FeedSubscription>)> {
let seen = progress.lock().unwrap_or_else(|p| p.into_inner()).clone();
let registry = registry.lock().unwrap_or_else(|p| p.into_inner());
registry
.iter()
.map(|(sub_id, record)| {
let targets: Vec<FeedTarget> = targets_of(&record.targets)
.iter()
.cloned()
.map(|target| resume_from(*sub_id, target, &seen))
.collect();
(*sub_id, feed_subscriptions(&targets))
})
.filter(|(_, requests)| !requests.is_empty())
.collect()
}
fn resume_from(
sub_id: u32,
mut target: FeedTarget,
seen: &HashMap<(u32, String), CandleResume>,
) -> FeedTarget {
if target.kind != EventKind::Candle {
return target;
}
if let Some(resume) = seen.get(&(sub_id, target.symbol.clone()))
&& let Some(through) = resume.through
{
target.from_time = Some(match target.from_time {
Some(original) => original.max(through.saturating_add(1)),
None => through.saturating_add(1),
});
}
target
}
fn event_symbol(event: &MarketEvent) -> Option<&str> {
Some(match event {
MarketEvent::Quote(quote) => "e.event_symbol,
MarketEvent::Trade(trade) => &trade.event_symbol,
MarketEvent::TradeETH(trade) => &trade.event_symbol,
MarketEvent::Greeks(greeks) => &greeks.event_symbol,
MarketEvent::Candle(candle) => &candle.event_symbol,
MarketEvent::Summary(summary) => &summary.event_symbol,
MarketEvent::TimeAndSale(sale) => &sale.event_symbol,
MarketEvent::Profile(profile) => &profile.event_symbol,
MarketEvent::Underlying(underlying) => &underlying.event_symbol,
MarketEvent::TheoPrice(theo) => &theo.event_symbol,
MarketEvent::Series(series) => &series.event_symbol,
})
}
fn event_kind(event: &MarketEvent) -> EventKind {
match event {
MarketEvent::Quote(_) => EventKind::Quote,
MarketEvent::Trade(_) => EventKind::Trade,
MarketEvent::TradeETH(_) => EventKind::TradeEth,
MarketEvent::Greeks(_) => EventKind::Greeks,
MarketEvent::Candle(_) => EventKind::Candle,
MarketEvent::Summary(_) => EventKind::Summary,
MarketEvent::TimeAndSale(_) => EventKind::TimeAndSale,
MarketEvent::Profile(_) => EventKind::Profile,
MarketEvent::Underlying(_) => EventKind::Underlying,
MarketEvent::TheoPrice(_) => EventKind::TheoPrice,
MarketEvent::Series(_) => EventKind::Series,
}
}
fn feed_event_type(kind: EventKind) -> EventType {
match kind {
EventKind::Quote => EventType::Quote,
EventKind::Trade => EventType::Trade,
EventKind::TradeEth => EventType::TradeETH,
EventKind::Greeks => EventType::Greeks,
EventKind::Candle => EventType::Candle,
EventKind::Summary => EventType::Summary,
EventKind::TimeAndSale => EventType::TimeAndSale,
EventKind::Profile => EventType::Profile,
EventKind::Underlying => EventType::Underlying,
EventKind::TheoPrice => EventType::TheoPrice,
EventKind::Series => EventType::Series,
}
}
async fn subscription_is_open(routing: &Arc<RwLock<EventRouting>>, sub_id: u32) -> bool {
routing.read().await.senders.contains_key(&sub_id)
}
fn withdrawn_request(
held: &Mutex<BTreeSet<FeedTarget>>,
requested: &[FeedSubscription],
) -> Option<String> {
let held = targets_of(held);
requested
.iter()
.find(|request| {
!held.iter().any(|target| {
target.symbol == request.symbol
&& target.kind.wire_name() == request.event_type
&& target.from_time == request.from_time
})
})
.map(|request| request.symbol.clone())
}
async fn record_routes(
routing: &Arc<RwLock<EventRouting>>,
sub_id: u32,
subscriptions: &[FeedSubscription],
) {
let mut routing = routing.write().await;
for route in routes_of(subscriptions) {
routing.routes.entry(route).or_default().insert(sub_id);
}
}
async fn orphaned_subscriptions(
routing: &Arc<RwLock<EventRouting>>,
sub_id: u32,
subscriptions: &[FeedSubscription],
) -> Vec<FeedSubscription> {
let routing = routing.read().await;
subscriptions
.iter()
.filter(|subscription| {
routes_of(std::slice::from_ref(*subscription))
.first()
.is_some_and(|route| {
routing
.routes
.get(route)
.is_some_and(|holders| holders.iter().all(|holder| *holder == sub_id))
})
})
.cloned()
.collect()
}
struct UnsubscribePlan {
released: Vec<FeedSubscription>,
orphaned: Vec<FeedSubscription>,
}
async fn plan_unsubscribe(
routing: &Arc<RwLock<EventRouting>>,
sub_id: u32,
still_held: Option<&Mutex<BTreeSet<FeedTarget>>>,
requested: &[FeedSubscription],
) -> UnsubscribePlan {
let released: Vec<FeedSubscription> = match still_held {
None => requested.to_vec(),
Some(targets) => {
let held = targets_of(targets);
requested
.iter()
.filter(|request| {
!held.iter().any(|target| {
target.symbol == request.symbol
&& target.kind.wire_name() == request.event_type
})
})
.cloned()
.collect()
}
};
let orphaned = orphaned_subscriptions(routing, sub_id, &released).await;
UnsubscribePlan { released, orphaned }
}
async fn forget_routes(
routing: &Arc<RwLock<EventRouting>>,
sub_id: u32,
subscriptions: &[FeedSubscription],
) {
let mut routing = routing.write().await;
for route in routes_of(subscriptions) {
if let Some(subs) = routing.routes.get_mut(&route) {
subs.remove(&sub_id);
if subs.is_empty() {
routing.routes.remove(&route);
}
}
}
}
fn routes_of(subscriptions: &[FeedSubscription]) -> Vec<(String, EventKind)> {
subscriptions
.iter()
.filter_map(|sub| {
EventKind::ALL
.iter()
.find(|kind| kind.wire_name() == sub.event_type)
.map(|kind| (sub.symbol.clone(), *kind))
})
.collect()
}
#[allow(clippy::too_many_arguments)]
async fn run_connection(
client: &mut DXLinkClient,
channel_id: u32,
commands: &mut mpsc::Receiver<DXLinkCommand>,
shutdown: &mut oneshot::Receiver<()>,
forwarder: &mut tokio::task::JoinHandle<()>,
routing: &Arc<RwLock<EventRouting>>,
configured: &mut BTreeSet<EventKind>,
dxlink_drops: &Arc<AtomicU64>,
) -> Ended {
loop {
dxlink_drops.store(client.dropped_event_count(), Ordering::Relaxed);
let cmd = tokio::select! {
biased;
() = tokio::time::sleep(DROP_MIRROR_INTERVAL) => {
dxlink_drops.store(client.dropped_event_count(), Ordering::Relaxed);
continue;
}
_ = &mut *shutdown => {
debug!("Quote streamer owner dropped, disconnecting");
return Ended::Owner;
}
joined = &mut *forwarder => {
match joined {
Ok(()) => debug!("The DXLink event stream closed; the session is over"),
Err(e) => warn!(
"The event forwarding task ended abnormally (panicked: {}, cancelled: {})",
e.is_panic(),
e.is_cancelled()
),
}
return Ended::ConnectionLost;
}
cmd = commands.recv() => match cmd {
Some(cmd) => cmd,
None => return Ended::Owner,
},
};
match cmd {
DXLinkCommand::Subscribe(subscriptions, kinds, sub_id, held, ack) => {
if !subscription_is_open(routing, sub_id).await {
answer(
ack,
Err(TastyTradeError::Streaming(
"the subscription was closed before the venue was asked; nothing \
was subscribed"
.to_string(),
)),
);
continue;
}
if let Some(symbol) = withdrawn_request(&held, &subscriptions) {
answer(
ack,
Err(TastyTradeError::Streaming(format!(
"{symbol} was removed from the subscription before the venue was \
asked; nothing was subscribed"
))),
);
continue;
}
if let Err(e) = ensure_configured(client, channel_id, configured, &kinds).await {
let lost = matches!(&e, TastyTradeError::Connection(_));
answer(ack, Err(e));
if lost {
return Ended::ConnectionLost;
}
continue;
}
record_routes(routing, sub_id, &subscriptions).await;
match client.subscribe(channel_id, subscriptions.clone()).await {
Ok(()) => answer(ack, Ok(())),
Err(e) => {
let lost = is_connection_lost(&e);
error!("Error subscribing to symbols: {}", e);
forget_routes(routing, sub_id, &subscriptions).await;
answer(
ack,
Err(TastyTradeError::Streaming(format!(
"the venue refused the subscription: {e}"
))),
);
if lost {
return Ended::ConnectionLost;
}
}
}
}
DXLinkCommand::Unsubscribe(subscriptions, sub_id, still_held, ack) => {
let plan =
plan_unsubscribe(routing, sub_id, still_held.as_deref(), &subscriptions).await;
if plan.orphaned.is_empty() {
forget_routes(routing, sub_id, &plan.released).await;
answer(ack, Ok(()));
continue;
}
let outcome = client.unsubscribe(channel_id, plan.orphaned).await;
if outcome.is_ok() {
forget_routes(routing, sub_id, &plan.released).await;
}
match outcome {
Ok(()) => answer(ack, Ok(())),
Err(e) => {
let lost = is_connection_lost(&e);
error!("Error unsubscribing from symbols: {}", e);
answer(
ack,
Err(TastyTradeError::Streaming(format!(
"the venue refused the unsubscribe: {e}"
))),
);
if lost {
return Ended::ConnectionLost;
}
}
}
}
DXLinkCommand::AddEventSender(subscription_id, sender) => {
let mut routing = routing.write().await;
routing
.senders
.entry(subscription_id)
.or_default()
.push(sender);
debug!("Added event sender for subscription {}", subscription_id);
}
DXLinkCommand::RemoveEventSender(subscription_id) => {
let mut routing = routing.write().await;
routing.senders.remove(&subscription_id);
routing.routes.retain(|_, subs| {
subs.remove(&subscription_id);
!subs.is_empty()
});
debug!("Removed event senders for subscription {}", subscription_id);
}
}
}
}
fn is_connection_lost(error: &dxlink::DXLinkError) -> bool {
matches!(
error,
dxlink::DXLinkError::Connection(_) | dxlink::DXLinkError::WebSocket(_)
)
}
async fn terminal(state: &Arc<RwLock<ConnectionState>>, reason: String) {
warn!("Quote stream gave up: {reason}");
*state.write().await = ConnectionState::Disconnected { reason };
}
async fn schedule(
policy: &BackoffPolicy,
attempt: &mut u32,
state: &Arc<RwLock<ConnectionState>>,
shutdown: &mut oneshot::Receiver<()>,
) -> bool {
*attempt = attempt.saturating_add(1);
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.subsec_nanos() as u64)
.unwrap_or(0);
let Some(delay) = policy.delay_for(*attempt, nanos) else {
terminal(state, format!("gave up after {} attempts", *attempt - 1)).await;
return false;
};
debug!("Quote stream reconnecting, attempt {attempt} in {delay:?}");
*state.write().await = ConnectionState::Reconnecting {
attempt: *attempt,
delay,
};
tokio::select! {
_ = &mut *shutdown => false,
_ = tokio::time::sleep(delay) => true,
}
}
#[allow(clippy::too_many_arguments)]
async fn supervise(
tasty: TastyTrade,
policy: BackoffPolicy,
first: LiveConnection,
mut commands: mpsc::Receiver<DXLinkCommand>,
mut shutdown: oneshot::Receiver<()>,
routing: Arc<RwLock<EventRouting>>,
registry: Registry,
state: Arc<RwLock<ConnectionState>>,
progress: CandleProgress,
drained: Arc<Notify>,
history: Arc<Notify>,
) {
let mut attempt = 0u32;
let mut next = Some(first);
let dxlink_drops = Arc::new(AtomicU64::new(0));
loop {
let LiveConnection {
mut client,
channel_id,
events,
} = match next.take() {
Some(connection) => connection,
None => match connect_dxlink(&tasty).await {
Ok(connection) => connection,
Err(e) => {
if !policy.should_retry(&e) {
terminal(&state, format!("reconnect refused: {e}")).await;
return;
}
if !schedule(&policy, &mut attempt, &state, &mut shutdown).await {
return;
}
continue;
}
},
};
let saw_event = Arc::new(AtomicBool::new(false));
dxlink_drops.store(0, Ordering::Relaxed);
let mut forwarder = tokio::spawn(forward_events(
events,
routing.clone(),
progress.clone(),
saw_event.clone(),
drained.clone(),
dxlink_drops.clone(),
history.clone(),
));
let mut configured: BTreeSet<EventKind> = BTreeSet::new();
let restored = replay(
&mut client,
channel_id,
®istry,
&progress,
&mut configured,
)
.await;
dxlink_drops.store(client.dropped_event_count(), Ordering::Relaxed);
if restored {
*state.write().await = ConnectionState::Connected;
}
let ended = if restored {
run_connection(
&mut client,
channel_id,
&mut commands,
&mut shutdown,
&mut forwarder,
&routing,
&mut configured,
&dxlink_drops,
)
.await
} else {
warn!("Could not restore every subscription; reconnecting");
Ended::ConnectionLost
};
forwarder.abort();
if !forwarder.is_finished() {
let _ = (&mut forwarder).await;
}
if matches!(ended, Ended::ConnectionLost) {
open_generations_for_the_next_connection(&progress, &routing, &history, &dxlink_drops)
.await;
}
if let Some(reason) = client.disconnect_reason() {
debug!("DXLink reported the session ended: {reason}");
}
if let Err(e) = client.disconnect().await {
debug!("Error disconnecting the previous DXLink client: {e}");
}
match ended {
Ended::Owner => {
*state.write().await = ConnectionState::Disconnected {
reason: "the streamer was dropped".to_string(),
};
debug!("DXLink supervisor terminated");
return;
}
Ended::ConnectionLost => {
if saw_event.load(Ordering::Relaxed) {
attempt = 0;
}
if !schedule(&policy, &mut attempt, &state, &mut shutdown).await {
return;
}
}
}
}
}
impl Drop for QuoteStreamer {
fn drop(&mut self) {
if let Some(shutdown) = self.shutdown.take() {
let _ = shutdown.send(());
}
}
}
#[cfg(test)]
mod lifecycle_tests {
use super::*;
use crate::api::quote_streaming::DxFeedSymbol;
#[tokio::test]
async fn both_copies_of_a_subscription_see_the_same_symbols() {
let (tx, rx) = mpsc::channel::<DXLinkCommand>(8);
let (shutdown_tx, _shutdown_rx) = oneshot::channel::<()>();
let mut streamer = streamer_with(tx, shutdown_tx);
let loop_handle = spawn_command_loop(rx, || Ok(()));
let sub = streamer
.create_sub([EventKind::Quote])
.await
.expect("the streamer is open");
sub.add_symbols(&[
DxFeedSymbol("AAPL".to_string()),
DxFeedSymbol("MSFT".to_string()),
])
.await
.expect("subscribing succeeds");
{
let stored = targets_of(
&streamer
.subscription_map
.get(&sub.id)
.expect("the streamer kept a copy")
.targets,
);
assert_eq!(stored.len(), 2, "the streamer's copy must see the symbols");
assert!(stored.iter().any(|target| target.symbol == "AAPL"));
}
drop(sub);
drop(streamer);
let sent = loop_handle.await.expect("the stand-in loop finishes");
assert!(sent.contains(&"AAPL".to_string()));
assert!(sent.contains(&"MSFT".to_string()));
}
#[tokio::test]
async fn a_refused_subscription_records_nothing() {
let (tx, rx) = mpsc::channel::<DXLinkCommand>(8);
let (shutdown_tx, _shutdown_rx) = oneshot::channel::<()>();
let mut streamer = streamer_with(tx, shutdown_tx);
let _loop_handle = spawn_command_loop(rx, || {
Err(TastyTradeError::Streaming("venue said no".to_string()))
});
let sub = streamer
.create_sub([EventKind::Quote])
.await
.expect("the streamer is open");
let error = sub
.add_symbols(&[DxFeedSymbol("AAPL".to_string())])
.await
.expect_err("a refused subscription is not a success");
assert!(format!("{error}").contains("venue said no"), "{error}");
assert!(
targets_of(&sub.targets).is_empty(),
"a refused symbol must not stay reserved"
);
}
#[tokio::test]
async fn a_repeated_symbol_is_not_subscribed_twice() {
let (tx, rx) = mpsc::channel::<DXLinkCommand>(8);
let (shutdown_tx, _shutdown_rx) = oneshot::channel::<()>();
let mut streamer = streamer_with(tx, shutdown_tx);
let loop_handle = spawn_command_loop(rx, || Ok(()));
let sub = streamer
.create_sub([EventKind::Quote])
.await
.expect("the streamer is open");
sub.add_symbols(&[DxFeedSymbol("AAPL".to_string())])
.await
.unwrap();
sub.add_symbols(&[DxFeedSymbol("AAPL".to_string())])
.await
.unwrap();
drop(sub);
drop(streamer);
let sent = loop_handle.await.expect("the stand-in loop finishes");
assert_eq!(
sent.len(),
1,
"the second request had nothing new to say: {sent:?}"
);
}
#[tokio::test]
async fn a_failed_subscribe_records_nothing() {
let (tx, rx) = mpsc::channel::<DXLinkCommand>(8);
let (shutdown_tx, _shutdown_rx) = oneshot::channel::<()>();
let mut streamer = streamer_with(tx, shutdown_tx);
let sub = streamer
.create_sub([EventKind::Quote])
.await
.expect("the streamer is open");
drop(rx);
sub.add_symbols(&[DxFeedSymbol("AAPL".to_string())])
.await
.expect_err("a closed streamer cannot subscribe");
assert!(
targets_of(&sub.targets).is_empty(),
"nothing was subscribed, so nothing may be recorded"
);
}
#[tokio::test]
async fn dropping_a_handle_does_not_disconnect_anyone() {
let (tx, mut rx) = mpsc::channel::<DXLinkCommand>(8);
let handle = StreamerHandle {
commands: Some(tx.clone()),
};
let second = handle.clone();
drop(handle);
drop(second);
assert!(
rx.try_recv().is_err(),
"dropping a handle must not send a command"
);
tx.send(DXLinkCommand::RemoveEventSender(9))
.await
.expect("the owner's channel is still alive after handles are dropped");
assert!(matches!(
rx.recv().await,
Some(DXLinkCommand::RemoveEventSender(9))
));
}
pub(super) fn spawn_command_loop(
mut rx: mpsc::Receiver<DXLinkCommand>,
outcome: fn() -> TastyResult<()>,
) -> tokio::task::JoinHandle<Vec<String>> {
tokio::spawn(async move {
let mut seen = Vec::new();
while let Some(cmd) = rx.recv().await {
match cmd {
DXLinkCommand::Subscribe(requests, _, _, _, ack) => {
seen.extend(requests.into_iter().map(|r| r.symbol));
answer(ack, outcome());
}
DXLinkCommand::Unsubscribe(_, _, _, ack) => answer(ack, outcome()),
_ => {}
}
}
seen
})
}
pub(super) fn streamer_with(
commands: mpsc::Sender<DXLinkCommand>,
shutdown: oneshot::Sender<()>,
) -> QuoteStreamer {
QuoteStreamer {
shutdown: Some(shutdown),
next_sub_id: 0,
subscription_map: HashMap::new(),
dxlink_command_tx: Some(commands),
registry: Arc::new(Mutex::new(HashMap::new())),
state: Arc::new(RwLock::new(ConnectionState::Connected)),
progress: Arc::new(Mutex::new(HashMap::new())),
drained: Arc::new(Notify::new()),
history: Arc::new(Notify::new()),
}
}
#[test]
fn dropping_the_owner_outside_a_runtime_does_not_panic() {
let (tx, _rx) = mpsc::channel::<DXLinkCommand>(8);
let (shutdown_tx, mut shutdown_rx) = oneshot::channel::<()>();
drop(streamer_with(tx, shutdown_tx));
assert!(
shutdown_rx.try_recv().is_ok(),
"the owner must signal shutdown on drop"
);
}
#[test]
fn shutdown_survives_a_full_command_queue() {
let (tx, _rx) = mpsc::channel::<DXLinkCommand>(1);
let (shutdown_tx, mut shutdown_rx) = oneshot::channel::<()>();
tx.try_send(DXLinkCommand::RemoveEventSender(9))
.expect("the first command fits");
assert!(
tx.try_send(DXLinkCommand::RemoveEventSender(9)).is_err(),
"the queue must actually be full for this test to mean anything"
);
drop(streamer_with(tx, shutdown_tx));
assert!(
shutdown_rx.try_recv().is_ok(),
"a full command queue must not be able to swallow the shutdown"
);
}
}
#[cfg(test)]
mod reconnect_tests {
use super::lifecycle_tests::{spawn_command_loop, streamer_with};
use super::*;
use crate::api::quote_streaming::DxFeedSymbol;
use dxlink::events::QuoteEvent;
use std::time::Duration;
fn quote(symbol: &str) -> MarketEvent {
MarketEvent::Quote(QuoteEvent {
event_type: "Quote".to_string(),
event_symbol: symbol.to_string(),
bid_price: 1.0,
ask_price: 2.0,
bid_size: 1.0,
ask_size: 1.0,
})
}
fn target(kind: EventKind, symbol: &str) -> FeedTarget {
FeedTarget {
kind,
symbol: symbol.to_string(),
from_time: None,
}
}
fn no_progress() -> CandleProgress {
Arc::new(Mutex::new(HashMap::new()))
}
fn sink(events: mpsc::Sender<Delivery>) -> Subscriber {
Subscriber {
events,
lagged: Arc::new(AtomicU64::new(0)),
pending: Arc::new(Mutex::new(VecDeque::new())),
}
}
fn market_of(delivery: Delivery) -> MarketEvent {
match delivery {
Delivery::Market(event) => event,
Delivery::Marker(marker) => {
panic!("expected market data, got the marker {:?}", marker.data)
}
}
}
fn marker_of(delivery: Delivery) -> dxfeed::Event {
match delivery {
Delivery::Marker(event) => event,
Delivery::Market(event) => panic!(
"expected a snapshot marker, got {} data for {:?}",
event_kind(&event),
event_symbol(&event)
),
}
}
fn flagged_candle(symbol: &str, time: i64, flags: i64) -> MarketEvent {
let MarketEvent::Candle(mut bar) = candle(symbol, time) else {
unreachable!("candle builds a candle")
};
bar.event_flags = flags;
MarketEvent::Candle(bar)
}
fn candle(symbol: &str, time: i64) -> MarketEvent {
let MarketEvent::Candle(mut candle) = every_event_type(symbol)
.into_iter()
.find(|event| matches!(event, MarketEvent::Candle(_)))
.expect("a candle")
else {
unreachable!("filtered on the variant")
};
candle.time = time;
MarketEvent::Candle(candle)
}
fn every_event_type(symbol: &str) -> Vec<MarketEvent> {
use dxlink::events::*;
let sym = || symbol.to_string();
vec![
quote(symbol),
MarketEvent::Trade(TradeEvent {
event_type: "Trade".to_string(),
event_symbol: sym(),
price: 1.0,
size: 1.0,
day_volume: 10.0,
}),
MarketEvent::TradeETH(TradeETHEvent {
event_type: "TradeETH".to_string(),
event_symbol: sym(),
event_time: 0,
time: 0,
time_nano_part: 0,
sequence: 0,
exchange_code: "Q".to_string(),
price: 1.0,
change: 0.0,
size: 1.0,
day_id: 0,
day_volume: 0.0,
day_turnover: 0.0,
tick_direction: "Up".to_string(),
extended_trading_hours: true,
}),
MarketEvent::Greeks(GreeksEvent {
event_type: "Greeks".to_string(),
event_symbol: sym(),
delta: 0.5,
gamma: 0.1,
theta: -0.05,
vega: 0.2,
rho: 0.03,
volatility: 0.25,
}),
MarketEvent::Candle(CandleEvent {
event_type: "Candle".to_string(),
event_symbol: sym(),
event_time: 0,
event_flags: 0,
index: 0,
time: 1_700_000_000_000,
sequence: 0,
count: 1,
open: 1.0,
high: 2.0,
low: 0.5,
close: 1.5,
volume: 100.0,
vwap: 1.4,
bid_volume: 50.0,
ask_volume: 50.0,
imp_volatility: 0.2,
open_interest: 0.0,
}),
MarketEvent::Summary(SummaryEvent {
event_type: "Summary".to_string(),
event_symbol: sym(),
event_time: 0,
day_id: 0,
day_open_price: 0.0,
day_high_price: 0.0,
day_low_price: 0.0,
day_close_price: 0.0,
day_close_price_type: "Final".to_string(),
prev_day_id: 0,
prev_day_close_price: 0.0,
prev_day_close_price_type: "Final".to_string(),
prev_day_volume: 0.0,
open_interest: 0.0,
}),
MarketEvent::TimeAndSale(TimeAndSaleEvent {
event_type: "TimeAndSale".to_string(),
event_symbol: sym(),
event_time: 0,
event_flags: 0,
index: 0,
time: 0,
time_nano_part: 0,
sequence: 0,
exchange_code: "Q".to_string(),
price: 1.0,
size: 1.0,
bid_price: 0.9,
ask_price: 1.1,
exchange_sale_conditions: String::new(),
trade_through_exempt: String::new(),
aggressor_side: "Buy".to_string(),
spread_leg: false,
extended_trading_hours: false,
valid_tick: true,
sale_type: String::new(),
buyer: String::new(),
seller: String::new(),
}),
MarketEvent::Profile(ProfileEvent {
event_type: "Profile".to_string(),
event_symbol: sym(),
event_time: 0,
description: "Apple".to_string(),
short_sale_restriction: "Inactive".to_string(),
trading_status: "Active".to_string(),
status_reason: String::new(),
halt_start_time: 0,
halt_end_time: 0,
high_limit_price: 0.0,
low_limit_price: 0.0,
high_52_week_price: 0.0,
low_52_week_price: 0.0,
beta: 0.0,
earnings_per_share: 0.0,
dividend_frequency: 0.0,
ex_dividend_amount: 0.0,
ex_dividend_day_id: 0,
shares: 0.0,
free_float: 0.0,
}),
MarketEvent::Underlying(UnderlyingEvent {
event_type: "Underlying".to_string(),
event_symbol: sym(),
event_time: 0,
event_flags: 0,
index: 0,
time: 0,
sequence: 0,
volatility: 0.2,
front_volatility: 0.21,
back_volatility: 0.19,
call_volume: 10.0,
put_volume: 8.0,
put_call_ratio: 0.8,
}),
MarketEvent::TheoPrice(TheoPriceEvent {
event_type: "TheoPrice".to_string(),
event_symbol: sym(),
event_time: 0,
event_flags: 0,
index: 0,
time: 0,
sequence: 0,
price: 1.0,
underlying_price: 100.0,
delta: 0.5,
gamma: 0.1,
dividend: 0.0,
interest: 0.0,
}),
MarketEvent::Series(SeriesEvent {
event_type: "Series".to_string(),
event_symbol: sym(),
event_time: 0,
event_flags: 0,
index: 0,
time: 0,
sequence: 0,
expiration: 20_260_918,
volatility: 0.2,
call_volume: 10.0,
put_volume: 8.0,
put_call_ratio: 0.8,
forward_price: 100.0,
dividend: 0.0,
interest: 0.0,
}),
]
}
fn policy() -> BackoffPolicy {
BackoffPolicy {
initial: Duration::from_millis(10),
max_delay: Duration::from_millis(40),
max_attempts: Some(2),
jitter: 0.0,
}
}
#[test]
fn a_replay_asks_for_the_event_types_the_subscription_had() {
let requests = feed_subscriptions(&[
target(EventKind::Quote, "AAPL"),
target(EventKind::Greeks, "AAPL"),
target(EventKind::Quote, "MSFT"),
target(EventKind::Greeks, "MSFT"),
]);
assert_eq!(requests.len(), 4, "two symbols by two event types");
let types: BTreeSet<&str> = requests.iter().map(|r| r.event_type.as_str()).collect();
assert_eq!(
types,
BTreeSet::from(["Greeks", "Quote"]),
"Trade was not asked for: {types:?}"
);
}
#[test]
fn a_subscription_with_no_confirmed_symbols_is_not_replayed() {
let registry: Registry = Arc::new(Mutex::new(HashMap::new()));
registry.lock().unwrap().insert(
7,
SubscriptionRecord {
kinds: BTreeSet::from([EventKind::Quote]),
targets: Arc::new(Mutex::new(BTreeSet::new())),
},
);
assert!(
pending_replay(®istry, &no_progress()).is_empty(),
"a refused subscribe records nothing, so there is nothing to restore"
);
}
#[tokio::test]
async fn a_closed_subscription_is_not_replayed() {
let (tx, rx) = mpsc::channel::<DXLinkCommand>(8);
let (shutdown_tx, _shutdown_rx) = oneshot::channel::<()>();
let mut streamer = streamer_with(tx, shutdown_tx);
let _loop_handle = spawn_command_loop(rx, || Ok(()));
let sub = streamer
.create_sub([EventKind::Quote])
.await
.expect("the streamer is open");
sub.add_symbols(&[DxFeedSymbol("AAPL".to_string())])
.await
.expect("subscribing succeeds");
let pending = pending_replay(&streamer.registry, &no_progress());
assert_eq!(pending.len(), 1, "a live subscription is restored");
assert_eq!(pending[0].1[0].symbol, "AAPL");
streamer.close_sub(sub.id).await.expect("closing succeeds");
assert!(
pending_replay(&streamer.registry, &no_progress()).is_empty(),
"a closed subscription must not be resubscribed by a reconnect"
);
}
#[tokio::test]
async fn a_forwarded_event_reaches_the_subscription_registered_for_it() {
let routing: Arc<RwLock<EventRouting>> = Arc::new(RwLock::new(EventRouting::default()));
let (sub_tx, mut sub_rx) = mpsc::channel::<Delivery>(4);
{
let mut routing = routing.write().await;
routing.senders.insert(1, vec![sink(sub_tx)]);
routing
.routes
.insert(("AAPL".to_string(), EventKind::Quote), HashSet::from([1]));
}
let (events_tx, events_rx) = mpsc::channel::<MarketEvent>(4);
let saw_event = Arc::new(AtomicBool::new(false));
let forwarder = tokio::spawn(forward_events(
events_rx,
routing.clone(),
no_progress(),
saw_event.clone(),
Arc::new(Notify::new()),
Arc::new(AtomicU64::new(0)),
Arc::new(Notify::new()),
));
events_tx
.send(quote("AAPL"))
.await
.expect("the feed accepts");
let received = sub_rx
.recv()
.await
.expect("the subscription is delivered to");
assert!(matches!(market_of(received), MarketEvent::Quote(q) if q.event_symbol == "AAPL"));
events_tx
.send(quote("TSLA"))
.await
.expect("the feed accepts");
assert!(
tokio::time::timeout(Duration::from_millis(50), sub_rx.recv())
.await
.is_err(),
"an unrouted symbol must not be delivered"
);
assert!(
saw_event.load(Ordering::Relaxed),
"an event that arrived is the milestone the backoff resets on"
);
forwarder.abort();
}
#[tokio::test]
async fn a_refused_subscribe_takes_its_route_back() {
let routing: Arc<RwLock<EventRouting>> = Arc::new(RwLock::new(EventRouting::default()));
let wanted = feed_subscriptions(&[target(EventKind::Quote, "AAPL")]);
record_routes(&routing, 3, &wanted).await;
record_routes(&routing, 4, &wanted).await;
forget_routes(&routing, 3, &wanted).await;
let routes = routing.read().await;
let subs = routes
.routes
.get(&("AAPL".to_string(), EventKind::Quote))
.expect("the accepted subscription still holds the symbol");
assert!(
!subs.contains(&3),
"a refused subscribe must not leave a route behind"
);
assert!(subs.contains(&4), "the accepted one keeps its route");
}
#[tokio::test]
async fn the_last_route_removed_takes_the_symbol_with_it() {
let routing: Arc<RwLock<EventRouting>> = Arc::new(RwLock::new(EventRouting::default()));
let wanted = feed_subscriptions(&[target(EventKind::Quote, "AAPL")]);
record_routes(&routing, 3, &wanted).await;
forget_routes(&routing, 3, &wanted).await;
assert!(
routing.read().await.routes.is_empty(),
"an empty set must not be left behind as a route"
);
}
#[tokio::test]
async fn a_closed_event_stream_ends_the_connection_without_any_write() {
let routing: Arc<RwLock<EventRouting>> = Arc::new(RwLock::new(EventRouting::default()));
let (events_tx, events_rx) = mpsc::channel::<MarketEvent>(4);
let saw_event = Arc::new(AtomicBool::new(false));
let mut forwarder = tokio::spawn(forward_events(
events_rx,
routing.clone(),
no_progress(),
saw_event.clone(),
Arc::new(Notify::new()),
Arc::new(AtomicU64::new(0)),
Arc::new(Notify::new()),
));
let mut client = DXLinkClient::new("wss://127.0.0.1:1", "unused");
let (_commands_tx, mut commands_rx) = mpsc::channel::<DXLinkCommand>(4);
let (_shutdown_tx, mut shutdown_rx) = oneshot::channel::<()>();
drop(events_tx);
let ended = tokio::time::timeout(
Duration::from_secs(2),
run_connection(
&mut client,
1,
&mut commands_rx,
&mut shutdown_rx,
&mut forwarder,
&routing,
&mut BTreeSet::new(),
&Arc::new(AtomicU64::new(0)),
),
)
.await
.expect("the closing stream must end the connection, not be waited on forever");
assert!(
matches!(ended, Ended::ConnectionLost),
"a closed event stream is a lost connection"
);
}
#[tokio::test]
async fn a_forwarder_that_panics_is_reported_rather_than_read_as_a_venue_drop() {
let routing: Arc<RwLock<EventRouting>> = Arc::new(RwLock::new(EventRouting::default()));
let mut forwarder = tokio::spawn(async { panic!("the forwarder died") });
let mut client = DXLinkClient::new("wss://127.0.0.1:1", "unused");
let (_commands_tx, mut commands_rx) = mpsc::channel::<DXLinkCommand>(4);
let (_shutdown_tx, mut shutdown_rx) = oneshot::channel::<()>();
let ended = tokio::time::timeout(
Duration::from_secs(2),
run_connection(
&mut client,
1,
&mut commands_rx,
&mut shutdown_rx,
&mut forwarder,
&routing,
&mut BTreeSet::new(),
&Arc::new(AtomicU64::new(0)),
),
)
.await
.expect("a dead forwarder must end the connection, not be waited on");
assert!(matches!(ended, Ended::ConnectionLost));
}
#[tokio::test]
async fn the_owner_still_takes_precedence_over_a_live_stream() {
let routing: Arc<RwLock<EventRouting>> = Arc::new(RwLock::new(EventRouting::default()));
let (_events_tx, events_rx) = mpsc::channel::<MarketEvent>(4);
let mut forwarder = tokio::spawn(forward_events(
events_rx,
routing.clone(),
no_progress(),
Arc::new(AtomicBool::new(false)),
Arc::new(Notify::new()),
Arc::new(AtomicU64::new(0)),
Arc::new(Notify::new()),
));
let mut client = DXLinkClient::new("wss://127.0.0.1:1", "unused");
let (_commands_tx, mut commands_rx) = mpsc::channel::<DXLinkCommand>(4);
let (shutdown_tx, mut shutdown_rx) = oneshot::channel::<()>();
drop(shutdown_tx);
let ended = tokio::time::timeout(
Duration::from_secs(2),
run_connection(
&mut client,
1,
&mut commands_rx,
&mut shutdown_rx,
&mut forwarder,
&routing,
&mut BTreeSet::new(),
&Arc::new(AtomicU64::new(0)),
),
)
.await
.expect("a dropped owner ends the connection promptly");
assert!(matches!(ended, Ended::Owner));
}
#[tokio::test]
async fn all_eleven_event_types_convert_and_are_delivered() {
let events = every_event_type("AAPL");
assert_eq!(events.len(), EventKind::ALL.len());
let (tx, rx) = mpsc::channel::<Delivery>(32);
let (_unused_tx, event_receiver) = flume::unbounded();
let mut subscription = QuoteSubscription {
id: SubscriptionId(0),
streamer: StreamerHandle { commands: None },
kinds: EventKind::ALL.into_iter().collect(),
event_receiver,
dxlink_receiver: rx,
targets: Arc::new(Mutex::new(BTreeSet::new())),
lagged: Arc::new(AtomicU64::new(0)),
progress: Arc::new(Mutex::new(HashMap::new())),
drained: Arc::new(Notify::new()),
history: Arc::new(Notify::new()),
closed: Arc::new(AtomicBool::new(false)),
};
for event in &events {
assert_eq!(event_symbol(event), Some("AAPL"), "{:?}", event_kind(event));
tx.send(Delivery::Market(event.clone()))
.await
.expect("the feed accepts");
}
let mut seen = BTreeSet::new();
for _ in 0..events.len() {
let event = tokio::time::timeout(Duration::from_secs(2), subscription.get_event())
.await
.expect("no modelled event may stall the reader")
.expect("every modelled event is readable");
assert_eq!(event.sym, "AAPL");
seen.insert(event.data.kind());
}
assert_eq!(
seen,
EventKind::ALL.into_iter().collect::<BTreeSet<_>>(),
"every event type must arrive as its own variant"
);
}
#[test]
fn the_kinds_this_crate_routes_cover_every_variant_the_feed_models() {
let kinds: BTreeSet<EventKind> = every_event_type("AAPL").iter().map(event_kind).collect();
assert_eq!(kinds, EventKind::ALL.into_iter().collect::<BTreeSet<_>>());
}
#[tokio::test]
async fn two_candle_periods_of_one_underlying_do_not_cross_deliver() {
let five = CandlePeriod::minutes(5).expect("a period");
let hour = CandlePeriod::hours(1).expect("a period");
let routing: Arc<RwLock<EventRouting>> = Arc::new(RwLock::new(EventRouting::default()));
let (five_tx, mut five_rx) = mpsc::channel::<Delivery>(4);
let (hour_tx, mut hour_rx) = mpsc::channel::<Delivery>(4);
{
let mut routing = routing.write().await;
routing.senders.insert(1, vec![sink(five_tx)]);
routing.senders.insert(2, vec![sink(hour_tx)]);
}
record_routes(
&routing,
1,
&feed_subscriptions(&[FeedTarget {
kind: EventKind::Candle,
symbol: five.streamer_symbol("AAPL"),
from_time: Some(0),
}]),
)
.await;
record_routes(
&routing,
2,
&feed_subscriptions(&[FeedTarget {
kind: EventKind::Candle,
symbol: hour.streamer_symbol("AAPL"),
from_time: Some(0),
}]),
)
.await;
let (events_tx, events_rx) = mpsc::channel::<MarketEvent>(4);
let progress = no_progress();
let forwarder = tokio::spawn(forward_events(
events_rx,
routing.clone(),
progress.clone(),
Arc::new(AtomicBool::new(false)),
Arc::new(Notify::new()),
Arc::new(AtomicU64::new(0)),
Arc::new(Notify::new()),
));
events_tx
.send(candle("AAPL{=h}", 3_600_000))
.await
.expect("the feed accepts");
let delivered = hour_rx.recv().await.expect("the hourly subscription");
assert_eq!(event_symbol(&market_of(delivered)), Some("AAPL{=h}"));
assert!(
tokio::time::timeout(Duration::from_millis(50), five_rx.recv())
.await
.is_err(),
"the five-minute subscription must not see an hourly bar"
);
events_tx
.send(candle("AAPL{=5m}", 300_000))
.await
.expect("the feed accepts");
let delivered = five_rx.recv().await.expect("the five-minute subscription");
assert_eq!(event_symbol(&market_of(delivered)), Some("AAPL{=5m}"));
assert!(
tokio::time::timeout(Duration::from_millis(50), hour_rx.recv())
.await
.is_err(),
"the hourly subscription must not see a five-minute bar"
);
events_tx
.send(quote("AAPL"))
.await
.expect("the feed accepts");
assert!(
tokio::time::timeout(Duration::from_millis(50), five_rx.recv())
.await
.is_err()
);
let seen = progress.lock().expect("not poisoned in tests").clone();
assert_eq!(
seen.get(&(1, "AAPL{=5m}".to_string()))
.and_then(|r| r.through),
Some(300_000)
);
assert_eq!(
seen.get(&(2, "AAPL{=h}".to_string()))
.and_then(|r| r.through),
Some(3_600_000)
);
forwarder.abort();
}
#[tokio::test]
async fn a_subscription_only_receives_the_event_types_it_asked_for() {
let routing: Arc<RwLock<EventRouting>> = Arc::new(RwLock::new(EventRouting::default()));
let (quotes_tx, mut quotes_rx) = mpsc::channel::<Delivery>(4);
routing
.write()
.await
.senders
.insert(1, vec![sink(quotes_tx)]);
record_routes(
&routing,
1,
&feed_subscriptions(&[target(EventKind::Quote, "AAPL")]),
)
.await;
let (events_tx, events_rx) = mpsc::channel::<MarketEvent>(4);
let forwarder = tokio::spawn(forward_events(
events_rx,
routing.clone(),
no_progress(),
Arc::new(AtomicBool::new(false)),
Arc::new(Notify::new()),
Arc::new(AtomicU64::new(0)),
Arc::new(Notify::new()),
));
let trade = every_event_type("AAPL")
.into_iter()
.find(|event| matches!(event, MarketEvent::Trade(_)))
.expect("a trade");
events_tx.send(trade).await.expect("the feed accepts");
assert!(
tokio::time::timeout(Duration::from_millis(50), quotes_rx.recv())
.await
.is_err(),
"a Quote subscription must not receive Trade prints"
);
events_tx
.send(quote("AAPL"))
.await
.expect("the feed accepts");
assert!(quotes_rx.recv().await.is_some(), "the quote still arrives");
forwarder.abort();
}
#[test]
fn a_candle_replay_resumes_after_the_last_contiguous_bar() {
let original = FeedTarget {
kind: EventKind::Candle,
symbol: "AAPL{=5m}".to_string(),
from_time: Some(1_000),
};
let seen = HashMap::from([(
(1u32, "AAPL{=5m}".to_string()),
CandleResume {
through: Some(5_000),
..CandleResume::new(0)
},
)]);
assert_eq!(
resume_from(1, original.clone(), &seen).from_time,
Some(5_001),
"the replay picks up one millisecond past the last bar delivered"
);
assert_eq!(
resume_from(1, original.clone(), &HashMap::new()).from_time,
Some(1_000)
);
assert_eq!(
resume_from(2, original.clone(), &seen).from_time,
Some(1_000)
);
let other = FeedTarget {
symbol: "AAPL{=h}".to_string(),
..original.clone()
};
assert_eq!(resume_from(1, other, &seen).from_time, Some(1_000));
assert_eq!(
resume_from(1, target(EventKind::Quote, "AAPL"), &seen).from_time,
None
);
}
#[test]
fn a_candle_request_carries_its_history_start() {
let requests = feed_subscriptions(&[FeedTarget {
kind: EventKind::Candle,
symbol: "AAPL{=5m}".to_string(),
from_time: Some(1_700_000_000_000),
}]);
assert_eq!(requests.len(), 1);
assert_eq!(requests[0].event_type, "Candle");
assert_eq!(requests[0].symbol, "AAPL{=5m}");
assert_eq!(requests[0].from_time, Some(1_700_000_000_000));
}
#[tokio::test]
async fn candles_and_bare_symbols_are_not_interchangeable() {
let (tx, rx) = mpsc::channel::<DXLinkCommand>(8);
let (shutdown_tx, _shutdown_rx) = oneshot::channel::<()>();
let mut streamer = streamer_with(tx, shutdown_tx);
let _loop_handle = spawn_command_loop(rx, || Ok(()));
let quotes = streamer
.create_sub([EventKind::Quote])
.await
.expect("the streamer is open");
let error = quotes
.add_candles(
&[DxFeedSymbol("AAPL".to_string())],
CandlePeriod::minutes(5).expect("a period"),
DateTime::from_timestamp(1_700_000_000, 0).expect("a timestamp"),
)
.await
.expect_err("the channel is not configured for candles");
assert!(
matches!(error, TastyTradeError::Precondition(_)),
"{error:?}"
);
let candles = streamer
.create_sub([EventKind::Candle])
.await
.expect("the streamer is open");
let error = candles
.add_symbols(&[DxFeedSymbol("AAPL".to_string())])
.await
.expect_err("a bare symbol has no period and no start time");
assert!(
matches!(error, TastyTradeError::Precondition(_)),
"{error:?}"
);
}
#[tokio::test]
async fn a_candle_subscription_is_recorded_under_its_period_symbol() {
let (tx, rx) = mpsc::channel::<DXLinkCommand>(8);
let (shutdown_tx, _shutdown_rx) = oneshot::channel::<()>();
let mut streamer = streamer_with(tx, shutdown_tx);
let loop_handle = spawn_command_loop(rx, || Ok(()));
let sub = streamer
.create_sub([EventKind::Candle])
.await
.expect("the streamer is open");
sub.add_candles(
&[DxFeedSymbol("AAPL".to_string())],
CandlePeriod::minutes(5).expect("a period"),
DateTime::from_timestamp(1_700_000_000, 0).expect("a timestamp"),
)
.await
.expect("subscribing succeeds");
assert_eq!(
sub.subscribed(),
vec![("AAPL{=5m}".to_string(), EventKind::Candle)]
);
drop(sub);
drop(streamer);
let sent = loop_handle.await.expect("the stand-in loop finishes");
assert_eq!(sent, vec!["AAPL{=5m}".to_string()]);
}
#[tokio::test]
async fn the_event_route_is_registered_before_a_subscription_can_be_used() {
let (tx, mut rx) = mpsc::channel::<DXLinkCommand>(8);
let (shutdown_tx, _shutdown_rx) = oneshot::channel::<()>();
let mut streamer = streamer_with(tx, shutdown_tx);
let sub = streamer
.create_sub([EventKind::Candle])
.await
.expect("the streamer is open");
let mut registrations = 0;
while let Ok(DXLinkCommand::AddEventSender(id, _)) = rx.try_recv() {
assert_eq!(id, sub.id.0 as u32);
registrations += 1;
}
assert_eq!(
registrations, 1,
"exactly one consumer is registered: the caller's. The streamer's own \
copy used to register a second that nothing could ever read, so it \
filled up and then charged a drop for every event afterwards"
);
}
#[tokio::test]
async fn creating_a_subscription_on_a_closed_streamer_fails() {
let (tx, rx) = mpsc::channel::<DXLinkCommand>(8);
let (shutdown_tx, _shutdown_rx) = oneshot::channel::<()>();
let mut streamer = streamer_with(tx, shutdown_tx);
drop(rx);
let Err(error) = streamer.create_sub([EventKind::Quote]).await else {
panic!("a closed streamer cannot register a route");
};
assert!(matches!(error, TastyTradeError::Streaming(_)), "{error:?}");
}
#[tokio::test]
async fn a_dropped_bar_freezes_the_resume_point_rather_than_being_stepped_over() {
let routing: Arc<RwLock<EventRouting>> = Arc::new(RwLock::new(EventRouting::default()));
let (full_tx, _never_read) = mpsc::channel::<Delivery>(1);
let lagged = Arc::new(AtomicU64::new(0));
routing.write().await.senders.insert(
1,
vec![Subscriber {
events: full_tx,
lagged: lagged.clone(),
pending: Arc::new(Mutex::new(VecDeque::new())),
}],
);
record_routes(
&routing,
1,
&feed_subscriptions(&[FeedTarget {
kind: EventKind::Candle,
symbol: "AAPL{=5m}".to_string(),
from_time: Some(0),
}]),
)
.await;
let (events_tx, events_rx) = mpsc::channel::<MarketEvent>(8);
let progress = no_progress();
let forwarder = tokio::spawn(forward_events(
events_rx,
routing.clone(),
progress.clone(),
Arc::new(AtomicBool::new(false)),
Arc::new(Notify::new()),
Arc::new(AtomicU64::new(0)),
Arc::new(Notify::new()),
));
for time in [1_000i64, 2_000, 3_000] {
events_tx
.send(candle("AAPL{=5m}", time))
.await
.expect("the feed accepts");
}
tokio::time::sleep(Duration::from_millis(100)).await;
let resume = progress
.lock()
.expect("not poisoned in tests")
.get(&(1, "AAPL{=5m}".to_string()))
.copied()
.expect("the series was seen");
assert_eq!(
resume.through,
Some(1_000),
"only the bar that was actually delivered may be resumed past"
);
assert!(
resume.gap,
"a drop has to be remembered, or the next delivery steps over it"
);
assert_eq!(lagged.load(Ordering::Relaxed), 2);
forwarder.abort();
}
#[tokio::test]
async fn a_one_minute_candle_is_routed_under_the_symbol_the_venue_echoes() {
let routing: Arc<RwLock<EventRouting>> = Arc::new(RwLock::new(EventRouting::default()));
let (sink_tx, mut sink_rx) = mpsc::channel::<Delivery>(8);
routing.write().await.senders.insert(1, vec![sink(sink_tx)]);
let period = CandlePeriod::minutes(1).expect("one minute is a period");
record_routes(
&routing,
1,
&feed_subscriptions(&[FeedTarget {
kind: EventKind::Candle,
symbol: period.streamer_symbol("AAPL"),
from_time: Some(0),
}]),
)
.await;
let (events_tx, events_rx) = mpsc::channel::<MarketEvent>(8);
let progress = no_progress();
let forwarder = tokio::spawn(forward_events(
events_rx,
routing.clone(),
progress.clone(),
Arc::new(AtomicBool::new(false)),
Arc::new(Notify::new()),
Arc::new(AtomicU64::new(0)),
Arc::new(Notify::new()),
));
events_tx
.send(candle("AAPL{=m}", 1_000))
.await
.expect("the feed accepts");
let delivered = tokio::time::timeout(Duration::from_secs(2), sink_rx.recv())
.await
.expect("the bar must be routed, not dropped as unregistered")
.expect("the sink is open");
assert_eq!(event_symbol(&market_of(delivered)), Some("AAPL{=m}"));
let seen = progress.lock().expect("not poisoned in tests").clone();
let resume = seen
.get(&(1, "AAPL{=m}".to_string()))
.copied()
.expect("the series was seen under its canonical symbol");
assert_eq!(resume.through, Some(1_000));
let replayed = resume_from(
1,
FeedTarget {
kind: EventKind::Candle,
symbol: period.streamer_symbol("AAPL"),
from_time: Some(0),
},
&seen,
);
assert_eq!(
replayed.from_time,
Some(1_001),
"the replay must continue past the delivered bar"
);
forwarder.abort();
}
#[tokio::test]
async fn canonical_candle_routes_are_removed_per_subscription() {
let routing: Arc<RwLock<EventRouting>> = Arc::new(RwLock::new(EventRouting::default()));
let period = CandlePeriod::minutes(1).expect("one minute is a period");
let wanted = feed_subscriptions(&[FeedTarget {
kind: EventKind::Candle,
symbol: period.streamer_symbol("AAPL"),
from_time: Some(0),
}]);
record_routes(&routing, 1, &wanted).await;
record_routes(&routing, 2, &wanted).await;
let key = ("AAPL{=m}".to_string(), EventKind::Candle);
assert_eq!(
routing.read().await.routes.get(&key).map(|s| s.len()),
Some(2),
"both subscriptions share the canonical route"
);
forget_routes(&routing, 1, &wanted).await;
assert_eq!(
routing.read().await.routes.get(&key).cloned(),
Some(HashSet::from([2])),
"the other subscriber keeps the route"
);
forget_routes(&routing, 2, &wanted).await;
assert!(
!routing.read().await.routes.contains_key(&key),
"nobody left, so the key goes"
);
}
#[tokio::test]
async fn a_dropped_one_minute_bar_freezes_the_canonical_resume_point() {
let routing: Arc<RwLock<EventRouting>> = Arc::new(RwLock::new(EventRouting::default()));
let (full_tx, _never_read) = mpsc::channel::<Delivery>(1);
routing.write().await.senders.insert(1, vec![sink(full_tx)]);
let period = CandlePeriod::minutes(1).expect("one minute is a period");
let target = FeedTarget {
kind: EventKind::Candle,
symbol: period.streamer_symbol("AAPL"),
from_time: Some(0),
};
record_routes(
&routing,
1,
&feed_subscriptions(std::slice::from_ref(&target)),
)
.await;
let (events_tx, events_rx) = mpsc::channel::<MarketEvent>(8);
let progress = no_progress();
let forwarder = tokio::spawn(forward_events(
events_rx,
routing.clone(),
progress.clone(),
Arc::new(AtomicBool::new(false)),
Arc::new(Notify::new()),
Arc::new(AtomicU64::new(0)),
Arc::new(Notify::new()),
));
for time in [1_000i64, 2_000] {
events_tx
.send(candle("AAPL{=m}", time))
.await
.expect("the feed accepts");
}
tokio::time::sleep(Duration::from_millis(100)).await;
let seen = progress.lock().expect("not poisoned in tests").clone();
let resume = seen
.get(&(1, "AAPL{=m}".to_string()))
.copied()
.expect("the series was seen");
assert_eq!(resume.through, Some(1_000));
assert!(resume.gap, "the drop is remembered");
assert_eq!(
resume_from(1, target, &seen).from_time,
Some(1_001),
"the replay resumes right after the last delivered bar"
);
forwarder.abort();
}
#[tokio::test]
async fn a_series_whose_first_bar_was_dropped_resumes_from_the_beginning() {
let routing: Arc<RwLock<EventRouting>> = Arc::new(RwLock::new(EventRouting::default()));
let (full_tx, _never_read) = mpsc::channel::<Delivery>(1);
full_tx
.try_send(Delivery::Market(quote("filler")))
.expect("the one slot is taken");
routing.write().await.senders.insert(
1,
vec![Subscriber {
events: full_tx,
lagged: Arc::new(AtomicU64::new(0)),
pending: Arc::new(Mutex::new(VecDeque::new())),
}],
);
record_routes(
&routing,
1,
&feed_subscriptions(&[FeedTarget {
kind: EventKind::Candle,
symbol: "AAPL{=5m}".to_string(),
from_time: Some(1_000),
}]),
)
.await;
let (events_tx, events_rx) = mpsc::channel::<MarketEvent>(8);
let progress = no_progress();
let forwarder = tokio::spawn(forward_events(
events_rx,
routing.clone(),
progress.clone(),
Arc::new(AtomicBool::new(false)),
Arc::new(Notify::new()),
Arc::new(AtomicU64::new(0)),
Arc::new(Notify::new()),
));
events_tx
.send(candle("AAPL{=5m}", 9_000))
.await
.expect("the feed accepts");
tokio::time::sleep(Duration::from_millis(100)).await;
let seen = progress.lock().expect("not poisoned in tests").clone();
assert_eq!(
seen.get(&(1, "AAPL{=5m}".to_string()))
.and_then(|r| r.through),
None,
"no bar was delivered, so there is nothing to resume past"
);
let target = FeedTarget {
kind: EventKind::Candle,
symbol: "AAPL{=5m}".to_string(),
from_time: Some(1_000),
};
assert_eq!(
resume_from(1, target, &seen).from_time,
Some(1_000),
"a series that has only ever dropped bars asks for the whole thing again"
);
forwarder.abort();
}
#[tokio::test]
async fn the_streamers_copy_reports_the_same_lag_as_the_callers() {
let (tx, mut rx) = mpsc::channel::<DXLinkCommand>(8);
let (shutdown_tx, _shutdown_rx) = oneshot::channel::<()>();
let mut streamer = streamer_with(tx, shutdown_tx);
let sub = streamer
.create_sub([EventKind::Quote])
.await
.expect("the streamer is open");
let Some(DXLinkCommand::AddEventSender(_, subscriber)) = rx.try_recv().ok() else {
panic!("the consumer is registered before create_sub returns");
};
subscriber.lagged.fetch_add(3, Ordering::Relaxed);
assert_eq!(sub.lagged(), 3, "the caller sees its own loss");
assert_eq!(
streamer
.get_sub(sub.id)
.expect("the streamer kept a copy")
.lagged(),
3,
"and so does the copy the streamer hands out"
);
}
#[test]
fn a_replay_resumes_from_before_the_gap() {
let seen = HashMap::from([(
(1u32, "AAPL{=5m}".to_string()),
CandleResume {
through: Some(1_000),
gap: true,
..CandleResume::new(0)
},
)]);
let target = FeedTarget {
kind: EventKind::Candle,
symbol: "AAPL{=5m}".to_string(),
from_time: Some(0),
};
assert_eq!(
resume_from(1, target, &seen).from_time,
Some(1_001),
"the replay comes back from before the gap and refills it"
);
}
#[tokio::test]
async fn a_subscription_with_no_buffer_is_refused() {
let (tx, _rx) = mpsc::channel::<DXLinkCommand>(8);
let (shutdown_tx, _shutdown_rx) = oneshot::channel::<()>();
let mut streamer = streamer_with(tx, shutdown_tx);
let Err(error) = streamer
.create_sub_with_capacity([EventKind::Quote], 0)
.await
else {
panic!("a zero-capacity subscription cannot deliver anything");
};
assert!(
matches!(error, TastyTradeError::Precondition(_)),
"{error:?}"
);
}
#[tokio::test]
async fn the_backoff_gives_up_and_says_why() {
let state = Arc::new(RwLock::new(ConnectionState::Connected));
let (_shutdown_tx, mut shutdown_rx) = oneshot::channel::<()>();
let mut attempt = 0u32;
assert!(schedule(&policy(), &mut attempt, &state, &mut shutdown_rx).await);
assert_eq!(
*state.read().await,
ConnectionState::Reconnecting {
attempt: 1,
delay: Duration::from_millis(10)
}
);
assert!(schedule(&policy(), &mut attempt, &state, &mut shutdown_rx).await);
assert!(
!schedule(&policy(), &mut attempt, &state, &mut shutdown_rx).await,
"one past the limit must stop"
);
let ConnectionState::Disconnected { reason } = state.read().await.clone() else {
panic!("giving up must be terminal, not another retry");
};
assert!(reason.contains("2 attempts"), "{reason}");
}
#[tokio::test]
async fn a_backoff_is_interrupted_by_the_owner_going_away() {
let state = Arc::new(RwLock::new(ConnectionState::Connected));
let (shutdown_tx, mut shutdown_rx) = oneshot::channel::<()>();
let slow = BackoffPolicy {
initial: Duration::from_secs(30),
max_attempts: None,
..policy()
};
let mut attempt = 0u32;
drop(shutdown_tx);
assert!(
!schedule(&slow, &mut attempt, &state, &mut shutdown_rx).await,
"a dropped owner ends the wait"
);
}
#[test]
fn only_a_dead_socket_counts_as_a_lost_connection() {
assert!(is_connection_lost(&dxlink::DXLinkError::Connection(
"closed".to_string()
)));
assert!(!is_connection_lost(&dxlink::DXLinkError::Protocol(
"unknown symbol".to_string()
)));
assert!(!is_connection_lost(&dxlink::DXLinkError::Authentication(
"token expired".to_string()
)));
}
#[test]
fn a_rejected_session_is_not_worth_retrying() {
let policy = BackoffPolicy::default();
let refused: TastyTradeError = dxlink::DXLinkError::Authentication("nope".into()).into();
assert!(!policy.should_retry(&refused), "{refused:?}");
assert!(policy.should_retry(&TastyTradeError::Connection("dropped".into())));
}
use snapshot_flags::{REMOVE_EVENT, SNAPSHOT_BEGIN, SNAPSHOT_END, SNAPSHOT_SNIP, TX_PENDING};
struct Harness {
routing: Arc<RwLock<EventRouting>>,
progress: CandleProgress,
drained: Arc<Notify>,
dxlink_drops: Arc<AtomicU64>,
history: Arc<Notify>,
events: mpsc::Sender<MarketEvent>,
forwarder: tokio::task::JoinHandle<()>,
}
impl Harness {
fn start() -> Self {
let routing: Arc<RwLock<EventRouting>> = Arc::new(RwLock::new(EventRouting::default()));
let progress = no_progress();
let drained = Arc::new(Notify::new());
let dxlink_drops = Arc::new(AtomicU64::new(0));
let history = Arc::new(Notify::new());
let (events, events_rx) = mpsc::channel::<MarketEvent>(64);
let forwarder = tokio::spawn(forward_events(
events_rx,
routing.clone(),
progress.clone(),
Arc::new(AtomicBool::new(false)),
drained.clone(),
dxlink_drops.clone(),
history.clone(),
));
Self {
routing,
progress,
drained,
dxlink_drops,
history,
events,
forwarder,
}
}
async fn watch(
&self,
sub_id: u32,
symbol: &str,
capacity: usize,
) -> (mpsc::Receiver<Delivery>, Arc<AtomicU64>) {
let (tx, rx) = mpsc::channel::<Delivery>(capacity);
let lagged = Arc::new(AtomicU64::new(0));
self.routing
.write()
.await
.senders
.entry(sub_id)
.or_default()
.push(Subscriber {
events: tx,
lagged: lagged.clone(),
pending: Arc::new(Mutex::new(VecDeque::new())),
});
record_routes(
&self.routing,
sub_id,
&feed_subscriptions(&[candle_target(symbol)]),
)
.await;
(rx, lagged)
}
async fn send(&self, event: MarketEvent) {
self.events.send(event).await.expect("the feed accepts");
}
fn resume_of(&self, sub_id: u32, symbol: &str) -> Option<CandleResume> {
self.progress
.lock()
.expect("not poisoned in tests")
.get(&(sub_id, symbol.to_string()))
.copied()
}
}
impl Drop for Harness {
fn drop(&mut self) {
self.forwarder.abort();
}
}
fn candle_target(symbol: &str) -> FeedTarget {
FeedTarget {
kind: EventKind::Candle,
symbol: symbol.to_string(),
from_time: Some(0),
}
}
async fn next(rx: &mut mpsc::Receiver<Delivery>) -> Delivery {
tokio::time::timeout(Duration::from_secs(2), rx.recv())
.await
.expect("timed out waiting for a delivery")
.expect("the consumer's channel is open")
}
async fn nothing_more(rx: &mut mpsc::Receiver<Delivery>) {
if let Ok(Some(delivery)) =
tokio::time::timeout(Duration::from_millis(150), rx.recv()).await
{
panic!("nothing should have arrived, got {delivery:?}");
}
}
fn begin_of(delivery: Delivery) -> dxfeed::DxfSnapshotBeginT {
match marker_of(delivery).data {
dxfeed::EventData::SnapshotBegin(begin) => begin,
other => panic!("expected a snapshot begin, got {other:?}"),
}
}
fn end_of(delivery: Delivery) -> dxfeed::DxfSnapshotEndT {
match marker_of(delivery).data {
dxfeed::EventData::SnapshotEnd(end) => end,
other => panic!("expected a snapshot end, got {other:?}"),
}
}
fn bar_time(delivery: Delivery) -> i64 {
match market_of(delivery) {
MarketEvent::Candle(bar) => bar.time,
other => panic!("expected a candle, got {other:?}"),
}
}
#[tokio::test]
async fn a_replay_ends_after_its_last_bar() {
let harness = Harness::start();
let (mut rx, lagged) = harness.watch(1, "AAPL{=5m}", 8).await;
harness
.send(flagged_candle("AAPL{=5m}", 1_000, SNAPSHOT_BEGIN))
.await;
harness.send(flagged_candle("AAPL{=5m}", 2_000, 0)).await;
harness
.send(flagged_candle("AAPL{=5m}", 3_000, SNAPSHOT_END))
.await;
assert_eq!(begin_of(next(&mut rx).await).generation, 1);
assert_eq!(bar_time(next(&mut rx).await), 1_000);
assert_eq!(bar_time(next(&mut rx).await), 2_000);
assert_eq!(bar_time(next(&mut rx).await), 3_000);
let end = end_of(next(&mut rx).await);
assert_eq!(end.generation, 1);
assert_eq!(end.kind, dxfeed::SnapshotEndKind::End);
assert!(end.lossless, "nothing was dropped");
assert_eq!(lagged.load(Ordering::Relaxed), 0);
assert_eq!(
harness.resume_of(1, "AAPL{=5m}").and_then(|r| r.through),
Some(3_000)
);
nothing_more(&mut rx).await;
}
#[tokio::test]
async fn a_snipped_replay_says_so() {
let harness = Harness::start();
let (mut rx, _) = harness.watch(1, "AAPL{=5m}", 8).await;
harness
.send(flagged_candle("AAPL{=5m}", 1_000, SNAPSHOT_BEGIN))
.await;
harness
.send(flagged_candle("AAPL{=5m}", 2_000, SNAPSHOT_SNIP))
.await;
let _ = begin_of(next(&mut rx).await);
let _ = bar_time(next(&mut rx).await);
let _ = bar_time(next(&mut rx).await);
assert_eq!(
end_of(next(&mut rx).await).kind,
dxfeed::SnapshotEndKind::Snip
);
}
#[tokio::test]
async fn an_empty_replay_delivers_markers_and_no_bar() {
let harness = Harness::start();
let (mut rx, _) = harness.watch(1, "AAPL{=5m}", 8).await;
harness
.send(flagged_candle(
"AAPL{=5m}",
0,
SNAPSHOT_BEGIN | SNAPSHOT_END | REMOVE_EVENT,
))
.await;
assert_eq!(begin_of(next(&mut rx).await).generation, 1);
let end = end_of(next(&mut rx).await);
assert_eq!(end.generation, 1);
assert!(end.lossless);
nothing_more(&mut rx).await;
assert_eq!(
harness.resume_of(1, "AAPL{=5m}").and_then(|r| r.through),
None,
"a placeholder timestamp must not become a resume point"
);
}
#[tokio::test]
async fn a_plain_remove_is_an_event_and_not_an_ending() {
let harness = Harness::start();
let (mut rx, _) = harness.watch(1, "AAPL{=5m}", 8).await;
harness
.send(flagged_candle("AAPL{=5m}", 1_000, SNAPSHOT_BEGIN))
.await;
harness
.send(flagged_candle("AAPL{=5m}", 2_000, REMOVE_EVENT))
.await;
let _ = begin_of(next(&mut rx).await);
assert_eq!(bar_time(next(&mut rx).await), 1_000);
assert_eq!(
bar_time(next(&mut rx).await),
2_000,
"a removal is still an event the consumer has to see"
);
nothing_more(&mut rx).await;
assert_eq!(
harness.resume_of(1, "AAPL{=5m}").map(|r| r.phase),
Some(SnapshotPhase::Open),
"the replay is still running"
);
}
#[tokio::test]
async fn a_re_emitted_terminator_ends_the_replay_once() {
let harness = Harness::start();
let (mut rx, _) = harness.watch(1, "AAPL{=5m}", 8).await;
harness
.send(flagged_candle("AAPL{=5m}", 1_000, SNAPSHOT_BEGIN))
.await;
harness
.send(flagged_candle("AAPL{=5m}", 2_000, SNAPSHOT_END))
.await;
harness
.send(flagged_candle("AAPL{=5m}", 0, SNAPSHOT_END | REMOVE_EVENT))
.await;
let _ = begin_of(next(&mut rx).await);
assert_eq!(bar_time(next(&mut rx).await), 1_000);
assert_eq!(bar_time(next(&mut rx).await), 2_000);
assert_eq!(end_of(next(&mut rx).await).generation, 1);
nothing_more(&mut rx).await;
}
#[tokio::test]
async fn a_terminator_inside_a_transaction_waits_for_it_to_close() {
let harness = Harness::start();
let (mut rx, _) = harness.watch(1, "AAPL{=5m}", 8).await;
harness
.send(flagged_candle("AAPL{=5m}", 1_000, SNAPSHOT_BEGIN))
.await;
harness
.send(flagged_candle(
"AAPL{=5m}",
2_000,
SNAPSHOT_END | TX_PENDING,
))
.await;
let _ = begin_of(next(&mut rx).await);
assert_eq!(bar_time(next(&mut rx).await), 1_000);
assert_eq!(bar_time(next(&mut rx).await), 2_000);
nothing_more(&mut rx).await;
assert_eq!(
harness.resume_of(1, "AAPL{=5m}").map(|r| r.phase),
Some(SnapshotPhase::Open),
"the transaction is still open, so the replay has not ended"
);
harness.send(flagged_candle("AAPL{=5m}", 3_000, 0)).await;
assert_eq!(bar_time(next(&mut rx).await), 3_000);
assert_eq!(end_of(next(&mut rx).await).generation, 1);
}
#[tokio::test]
async fn a_slow_consumer_is_told_its_history_is_incomplete() {
let harness = Harness::start();
let (mut rx, lagged) = harness.watch(1, "AAPL{=5m}", 2).await;
harness
.send(flagged_candle("AAPL{=5m}", 1_000, SNAPSHOT_BEGIN))
.await;
for time in [2_000i64, 3_000, 4_000, 5_000] {
harness.send(flagged_candle("AAPL{=5m}", time, 0)).await;
}
harness
.send(flagged_candle(
"AAPL{=5m}",
6_000,
SNAPSHOT_END | REMOVE_EVENT,
))
.await;
assert_eq!(begin_of(next(&mut rx).await).generation, 1);
assert_eq!(bar_time(next(&mut rx).await), 1_000);
harness.drained.notify_one();
let end = end_of(next(&mut rx).await);
assert!(
!end.lossless,
"bars of this replay never reached the consumer"
);
assert_eq!(end.generation, 1);
assert!(lagged.load(Ordering::Relaxed) >= 1, "the loss is countable");
}
#[tokio::test]
async fn a_full_queue_parks_the_ending_instead_of_losing_it() {
let harness = Harness::start();
let (mut rx, lagged) = harness.watch(1, "AAPL{=5m}", 2).await;
harness
.send(flagged_candle("AAPL{=5m}", 1_000, SNAPSHOT_BEGIN))
.await;
harness.send(flagged_candle("AAPL{=5m}", 2_000, 0)).await;
harness
.send(flagged_candle(
"AAPL{=5m}",
3_000,
SNAPSHOT_END | REMOVE_EVENT,
))
.await;
harness.send(flagged_candle("AAPL{=5m}", 4_000, 0)).await;
assert_eq!(begin_of(next(&mut rx).await).generation, 1);
harness.drained.notify_one();
assert_eq!(bar_time(next(&mut rx).await), 1_000);
harness.drained.notify_one();
assert_eq!(
end_of(next(&mut rx).await).generation,
1,
"the ending survived a full queue"
);
assert_eq!(
lagged.load(Ordering::Relaxed),
2,
"both live bars were dropped rather than overtaking the ending"
);
harness.send(flagged_candle("AAPL{=5m}", 5_000, 0)).await;
assert_eq!(bar_time(next(&mut rx).await), 5_000);
}
#[tokio::test]
async fn loss_inside_the_feed_client_makes_a_replay_lossy() {
let harness = Harness::start();
let (mut rx, lagged) = harness.watch(1, "AAPL{=5m}", 8).await;
harness
.send(flagged_candle("AAPL{=5m}", 1_000, SNAPSHOT_BEGIN))
.await;
let _ = begin_of(next(&mut rx).await);
let _ = bar_time(next(&mut rx).await);
harness.dxlink_drops.store(3, Ordering::Relaxed);
harness
.send(flagged_candle("AAPL{=5m}", 2_000, SNAPSHOT_END))
.await;
let _ = bar_time(next(&mut rx).await);
let end = end_of(next(&mut rx).await);
assert!(
!end.lossless,
"events the feed client dropped are still missing history"
);
assert_eq!(
lagged.load(Ordering::Relaxed),
0,
"this consumer kept up; the loss was above it"
);
}
#[tokio::test]
async fn a_reconnect_starts_a_new_generation_without_waiting_for_the_venue() {
let harness = Harness::start();
let (mut rx, _) = harness.watch(1, "AAPL{=5m}", 16).await;
harness
.send(flagged_candle("AAPL{=5m}", 1_000, SNAPSHOT_BEGIN))
.await;
harness
.send(flagged_candle("AAPL{=5m}", 2_000, SNAPSHOT_END))
.await;
tokio::time::sleep(Duration::from_millis(50)).await;
assert_eq!(
harness.resume_of(1, "AAPL{=5m}").map(|r| r.phase),
Some(SnapshotPhase::Ended)
);
invalidate_history(
&harness.progress,
&harness.routing,
&harness.history,
&harness.dxlink_drops,
)
.await;
assert_eq!(
harness.resume_of(1, "AAPL{=5m}").map(|r| r.phase),
Some(SnapshotPhase::Open),
"history stops being loaded the moment the socket drops"
);
assert_eq!(begin_of(next(&mut rx).await).generation, 1);
assert_eq!(bar_time(next(&mut rx).await), 1_000);
assert_eq!(bar_time(next(&mut rx).await), 2_000);
assert_eq!(
end_of(next(&mut rx).await).generation,
1,
"an ending from the old replay is identifiable, not lost"
);
assert_eq!(begin_of(next(&mut rx).await).generation, 2);
harness
.send(flagged_candle("AAPL{=5m}", 3_000, SNAPSHOT_BEGIN))
.await;
assert_eq!(bar_time(next(&mut rx).await), 3_000);
harness
.send(flagged_candle("AAPL{=5m}", 4_000, SNAPSHOT_END))
.await;
assert_eq!(bar_time(next(&mut rx).await), 4_000);
assert_eq!(end_of(next(&mut rx).await).generation, 2);
nothing_more(&mut rx).await;
}
async fn also_watch(harness: &Harness, sub_id: u32, symbol: &str) {
record_routes(
&harness.routing,
sub_id,
&feed_subscriptions(&[candle_target(symbol)]),
)
.await;
}
fn spawn_routing_command_loop(
rx: mpsc::Receiver<DXLinkCommand>,
routing: Arc<RwLock<EventRouting>>,
) -> tokio::task::JoinHandle<()> {
let (handle, _) = spawn_recording_command_loop(rx, routing);
handle
}
fn spawn_recording_command_loop(
mut rx: mpsc::Receiver<DXLinkCommand>,
routing: Arc<RwLock<EventRouting>>,
) -> (tokio::task::JoinHandle<()>, Arc<Mutex<Vec<String>>>) {
let unsubscribed: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let seen = unsubscribed.clone();
let handle = tokio::spawn(async move {
while let Some(cmd) = rx.recv().await {
match cmd {
DXLinkCommand::Subscribe(requests, _, sub_id, held, ack) => {
if let Some(symbol) = withdrawn_request(&held, &requests) {
answer(ack, Err(TastyTradeError::Streaming(symbol)));
continue;
}
record_routes(&routing, sub_id, &requests).await;
answer(ack, Ok(()));
}
DXLinkCommand::Unsubscribe(requests, sub_id, still_held, ack) => {
let plan =
plan_unsubscribe(&routing, sub_id, still_held.as_deref(), &requests)
.await;
seen.lock()
.expect("not poisoned in tests")
.extend(plan.orphaned.into_iter().map(|request| request.symbol));
forget_routes(&routing, sub_id, &plan.released).await;
answer(ack, Ok(()));
}
_ => {}
}
}
});
(handle, unsubscribed)
}
fn subscription_for(
sub_id: u32,
commands: mpsc::Sender<DXLinkCommand>,
harness: &Harness,
targets: Arc<Mutex<BTreeSet<FeedTarget>>>,
) -> QuoteSubscription {
subscription_with(
sub_id,
commands,
harness.progress.clone(),
harness.drained.clone(),
harness.history.clone(),
targets,
)
}
fn subscription_with(
sub_id: u32,
commands: mpsc::Sender<DXLinkCommand>,
progress: CandleProgress,
drained: Arc<Notify>,
history: Arc<Notify>,
targets: Arc<Mutex<BTreeSet<FeedTarget>>>,
) -> QuoteSubscription {
let (_closed, closed_rx) = mpsc::channel::<Delivery>(1);
let (_unused, event_receiver) = flume::unbounded();
QuoteSubscription {
id: SubscriptionId(sub_id as usize),
streamer: StreamerHandle {
commands: Some(commands),
},
kinds: BTreeSet::from([EventKind::Candle]),
event_receiver,
dxlink_receiver: closed_rx,
targets,
lagged: Arc::new(AtomicU64::new(0)),
progress,
drained,
history,
closed: Arc::new(AtomicBool::new(false)),
}
}
fn shared_targets(symbols: &[&str]) -> Arc<Mutex<BTreeSet<FeedTarget>>> {
Arc::new(Mutex::new(
symbols.iter().map(|symbol| candle_target(symbol)).collect(),
))
}
#[tokio::test]
async fn await_history_resolves_when_the_replay_ends() {
let harness = Harness::start();
let (_rx, _) = harness.watch(1, "AAPL{=5m}", 8).await;
let (commands, _command_rx) = mpsc::channel::<DXLinkCommand>(4);
let subscription = subscription_for(1, commands, &harness, shared_targets(&["AAPL{=5m}"]));
assert!(
!subscription.history_loaded("AAPL{=5m}"),
"nothing has replayed yet"
);
let feed = async {
harness
.send(flagged_candle("AAPL{=5m}", 1_000, SNAPSHOT_BEGIN))
.await;
harness
.send(flagged_candle("AAPL{=5m}", 2_000, SNAPSHOT_END))
.await;
};
let waiting = tokio::time::timeout(
Duration::from_secs(2),
subscription.await_history("AAPL{=5m}"),
);
let (_, resolved) = tokio::join!(feed, waiting);
let end = resolved
.expect("the wait must not hang")
.expect("the series is subscribed");
assert_eq!(end.generation, 1);
assert_eq!(end.kind, dxfeed::SnapshotEndKind::End);
assert!(end.lossless);
assert!(subscription.history_loaded("AAPL{=5m}"));
let again = tokio::time::timeout(
Duration::from_secs(2),
subscription.await_history("AAPL{=5m}"),
)
.await
.expect("an answer that is already known must not wait");
assert_eq!(again.expect("still subscribed").generation, 1);
}
#[tokio::test]
async fn await_history_refuses_a_series_this_subscription_does_not_hold() {
let harness = Harness::start();
let (commands, _command_rx) = mpsc::channel::<DXLinkCommand>(4);
let subscription = subscription_for(1, commands, &harness, shared_targets(&["AAPL{=5m}"]));
let error = tokio::time::timeout(
Duration::from_secs(2),
subscription.await_history("AAPL"),
)
.await
.expect("it must answer rather than wait")
.expect_err("an unsubscribed series can never finish");
assert!(matches!(error, TastyTradeError::Precondition(_)));
}
#[tokio::test]
async fn a_reconnect_makes_a_finished_history_unfinished_again() {
let harness = Harness::start();
let (_rx, _) = harness.watch(1, "AAPL{=5m}", 16).await;
let (commands, _command_rx) = mpsc::channel::<DXLinkCommand>(4);
let subscription = subscription_for(1, commands, &harness, shared_targets(&["AAPL{=5m}"]));
harness
.send(flagged_candle("AAPL{=5m}", 1_000, SNAPSHOT_BEGIN))
.await;
harness
.send(flagged_candle("AAPL{=5m}", 2_000, SNAPSHOT_END))
.await;
let end = tokio::time::timeout(
Duration::from_secs(2),
subscription.await_history("AAPL{=5m}"),
)
.await
.expect("the wait must not hang")
.expect("the series is subscribed");
assert_eq!(end.generation, 1);
invalidate_history(
&harness.progress,
&harness.routing,
&harness.history,
&harness.dxlink_drops,
)
.await;
assert!(
!subscription.history_loaded("AAPL{=5m}"),
"the replay that finished is not the current one any more"
);
assert!(
tokio::time::timeout(
Duration::from_millis(150),
subscription.await_history("AAPL{=5m}")
)
.await
.is_err(),
"a waiter must not be satisfied by the superseded generation"
);
harness
.send(flagged_candle("AAPL{=5m}", 3_000, SNAPSHOT_END))
.await;
let end = tokio::time::timeout(
Duration::from_secs(2),
subscription.await_history("AAPL{=5m}"),
)
.await
.expect("the wait must not hang")
.expect("the series is subscribed");
assert_eq!(end.generation, 2, "the replay the reconnect started");
}
#[tokio::test]
async fn a_finished_series_is_dropped_without_disturbing_the_others() {
let harness = Harness::start();
let (mut mine, _) = harness.watch(1, "AAPL{=5m}", 64).await;
also_watch(&harness, 1, "AAPL{=h}").await;
also_watch(&harness, 1, "MSFT{=5m}").await;
let (mut theirs, _) = harness.watch(2, "AAPL{=5m}", 64).await;
let (commands, command_rx) = mpsc::channel::<DXLinkCommand>(8);
let loop_handle = spawn_routing_command_loop(command_rx, harness.routing.clone());
let mine_targets = shared_targets(&["AAPL{=5m}", "AAPL{=h}", "MSFT{=5m}"]);
let theirs_targets = shared_targets(&["AAPL{=5m}"]);
let subscription = subscription_for(1, commands, &harness, mine_targets.clone());
for symbol in ["AAPL{=5m}", "AAPL{=h}", "MSFT{=5m}"] {
harness
.send(flagged_candle(symbol, 1_000, SNAPSHOT_BEGIN))
.await;
harness
.send(flagged_candle(symbol, 2_000, SNAPSHOT_END))
.await;
}
let mut five_minute_series = None;
for symbol in ["AAPL{=5m}", "AAPL{=h}", "MSFT{=5m}"] {
assert_eq!(begin_of(next(&mut mine).await).generation, 1, "{symbol}");
assert_eq!(bar_time(next(&mut mine).await), 1_000, "{symbol}");
assert_eq!(bar_time(next(&mut mine).await), 2_000, "{symbol}");
let ending = marker_of(next(&mut mine).await);
assert_eq!(ending.sym, symbol, "a marker names its own series");
match ending.data {
dxfeed::EventData::SnapshotEnd(end) => {
assert_eq!(end.generation, 1, "{symbol}")
}
other => panic!("expected a snapshot end for {symbol}, got {other:?}"),
}
assert!(subscription.history_loaded(symbol), "{symbol}");
if symbol == "AAPL{=5m}" {
five_minute_series = Some(ending.sym);
}
}
let _ = begin_of(next(&mut theirs).await);
assert_eq!(bar_time(next(&mut theirs).await), 1_000);
assert_eq!(bar_time(next(&mut theirs).await), 2_000);
assert_eq!(end_of(next(&mut theirs).await).generation, 1);
let five = CandlePeriod::minutes(5).expect("a period");
let wire = five_minute_series.expect("the five-minute replay ended");
let base = five
.base_symbol(&wire)
.expect("the marker names a five-minute series");
assert_eq!(base, DxFeedSymbol("AAPL".to_string()));
subscription
.remove_candles(&[base], five)
.await
.expect("the venue accepted the unsubscribe");
let left: Vec<String> = subscription
.subscribed()
.into_iter()
.map(|(symbol, _)| symbol)
.collect();
assert_eq!(
left,
vec!["AAPL{=h}".to_string(), "MSFT{=5m}".to_string()],
"only the removed series goes"
);
let forgotten = harness
.resume_of(1, "AAPL{=5m}")
.expect("the generation counter outlives the removal");
assert_eq!(
forgotten.phase,
SnapshotPhase::Idle,
"a resubscribe must not inherit a finished replay"
);
assert_eq!(
forgotten.generation, 1,
"the counter is kept, so a later replay cannot reuse a number the \
consumer has already seen"
);
harness.send(flagged_candle("AAPL{=5m}", 3_000, 0)).await;
harness.send(flagged_candle("AAPL{=h}", 3_000, 0)).await;
assert_eq!(
bar_time(next(&mut theirs).await),
3_000,
"the other subscription still gets the series it shares"
);
assert_eq!(
bar_time(next(&mut mine).await),
3_000,
"the first event to arrive here must be the series that was kept"
);
assert_eq!(
harness.resume_of(2, "AAPL{=5m}").map(|r| r.phase),
Some(SnapshotPhase::Ended),
"removing a series from one subscription leaves another's alone"
);
nothing_more(&mut mine).await;
let registry: Registry = Arc::new(Mutex::new(HashMap::from([
(
1u32,
SubscriptionRecord {
kinds: BTreeSet::from([EventKind::Candle]),
targets: mine_targets,
},
),
(
2u32,
SubscriptionRecord {
kinds: BTreeSet::from([EventKind::Candle]),
targets: theirs_targets,
},
),
])));
let replay: HashMap<u32, Vec<String>> = pending_replay(®istry, &harness.progress)
.into_iter()
.map(|(sub_id, requests)| {
(
sub_id,
requests.into_iter().map(|request| request.symbol).collect(),
)
})
.collect();
assert_eq!(
replay.get(&1).expect("the subscription is replayed"),
&vec!["AAPL{=h}".to_string(), "MSFT{=5m}".to_string()],
"the removed series must not be resubscribed"
);
assert_eq!(
replay.get(&2).expect("the other subscription is replayed"),
&vec!["AAPL{=5m}".to_string()],
"and the subscription that kept it still gets it back"
);
loop_handle.abort();
}
#[tokio::test]
async fn a_live_bar_dropped_after_the_replay_does_not_rewrite_its_answer() {
let harness = Harness::start();
let (_rx, lagged) = harness.watch(1, "AAPL{=5m}", 3).await;
harness
.send(flagged_candle("AAPL{=5m}", 1_000, SNAPSHOT_BEGIN))
.await;
harness
.send(flagged_candle(
"AAPL{=5m}",
2_000,
SNAPSHOT_END | REMOVE_EVENT,
))
.await;
tokio::time::sleep(Duration::from_millis(100)).await;
let ended = harness
.resume_of(1, "AAPL{=5m}")
.expect("the series was seen");
assert_eq!(ended.phase, SnapshotPhase::Ended);
assert!(ended.lossless, "nothing was dropped during the replay");
harness.send(flagged_candle("AAPL{=5m}", 3_000, 0)).await;
tokio::time::sleep(Duration::from_millis(100)).await;
assert_eq!(
lagged.load(Ordering::Relaxed),
1,
"the live bar was dropped"
);
assert!(
harness
.resume_of(1, "AAPL{=5m}")
.expect("the series is still known")
.lossless,
"the finished replay's answer must not change afterwards"
);
}
#[tokio::test]
async fn await_history_gives_up_when_the_series_is_removed() {
let harness = Harness::start();
let (_rx, _) = harness.watch(1, "AAPL{=5m}", 8).await;
let (commands, command_rx) = mpsc::channel::<DXLinkCommand>(8);
let loop_handle = spawn_routing_command_loop(command_rx, harness.routing.clone());
let subscription = subscription_for(1, commands, &harness, shared_targets(&["AAPL{=5m}"]));
let waiting = tokio::time::timeout(
Duration::from_secs(2),
subscription.await_history("AAPL{=5m}"),
);
let removing = async {
tokio::time::sleep(Duration::from_millis(50)).await;
subscription
.remove_candles(
&[DxFeedSymbol("AAPL".to_string())],
CandlePeriod::minutes(5).expect("a period"),
)
.await
};
let (waited, removed) = tokio::join!(waiting, removing);
removed.expect("the venue accepted the unsubscribe");
let error = waited
.expect("the wait must end rather than hang")
.expect_err("a series nobody holds can never finish");
assert!(matches!(error, TastyTradeError::Precondition(_)));
loop_handle.abort();
}
#[tokio::test]
async fn closing_a_subscription_forgets_its_history() {
let (tx, rx) = mpsc::channel::<DXLinkCommand>(8);
let (shutdown_tx, _shutdown_rx) = oneshot::channel::<()>();
let loop_handle = spawn_command_loop(rx, || Ok(()));
let mut streamer = streamer_with(tx.clone(), shutdown_tx);
streamer
.progress
.lock()
.expect("not poisoned in tests")
.insert(
(0u32, "AAPL{=5m}".to_string()),
CandleResume {
generation: 1,
phase: SnapshotPhase::Ended,
ended_as: Some(dxfeed::SnapshotEndKind::End),
..CandleResume::new(0)
},
);
let subscription = subscription_with(
0,
tx,
streamer.progress.clone(),
streamer.drained.clone(),
streamer.history.clone(),
shared_targets(&["AAPL{=5m}"]),
);
assert!(subscription.history_loaded("AAPL{=5m}"));
let id = SubscriptionId(0);
streamer.subscription_map.insert(id, subscription);
streamer
.close_sub(id)
.await
.expect("the venue accepted the close");
assert!(
streamer
.progress
.lock()
.expect("not poisoned in tests")
.is_empty(),
"a closed subscription must not keep answering for its series"
);
loop_handle.abort();
}
#[tokio::test]
async fn a_futures_series_round_trips_without_being_rewritten() {
const CONTRACT: &str = "/ESU23:XCME";
let hourly = CandlePeriod::hours(1).expect("a period");
let five = CandlePeriod::minutes(5).expect("a period");
let hourly_wire = hourly.streamer_symbol(CONTRACT);
let five_wire = five.streamer_symbol(CONTRACT);
assert_eq!(hourly_wire, "/ESU23:XCME{=h}");
assert_eq!(five_wire, "/ESU23:XCME{=5m}");
let harness = Harness::start();
let (mut mine, _) = harness.watch(1, &hourly_wire, 32).await;
also_watch(&harness, 1, &five_wire).await;
let (commands, command_rx) = mpsc::channel::<DXLinkCommand>(8);
let loop_handle = spawn_routing_command_loop(command_rx, harness.routing.clone());
let subscription = subscription_for(
1,
commands,
&harness,
shared_targets(&[&hourly_wire, &five_wire]),
);
for symbol in [&hourly_wire, &five_wire] {
harness
.send(flagged_candle(symbol, 1_000, SNAPSHOT_BEGIN))
.await;
harness
.send(flagged_candle(symbol, 2_000, SNAPSHOT_END))
.await;
}
assert_eq!(begin_of(next(&mut mine).await).generation, 1);
assert_eq!(bar_time(next(&mut mine).await), 1_000);
assert_eq!(bar_time(next(&mut mine).await), 2_000);
let ending = marker_of(next(&mut mine).await);
assert_eq!(
ending.sym, hourly_wire,
"the marker must carry the venue's own string, not a rewritten one"
);
let _ = begin_of(next(&mut mine).await);
assert_eq!(bar_time(next(&mut mine).await), 1_000);
assert_eq!(bar_time(next(&mut mine).await), 2_000);
assert_eq!(marker_of(next(&mut mine).await).sym, five_wire);
assert!(subscription.history_loaded(&hourly_wire));
assert!(subscription.history_loaded(&five_wire));
assert!(
!subscription.history_loaded(CONTRACT),
"the bare contract names no series on its own"
);
let awaited = tokio::time::timeout(
Duration::from_secs(2),
subscription.await_history(&hourly_wire),
)
.await
.expect("already finished, so it must not wait")
.expect("the series is subscribed");
assert_eq!(awaited.generation, 1);
let base = hourly
.base_symbol(&ending.sym)
.expect("the marker names an hourly series");
assert_eq!(base, DxFeedSymbol(CONTRACT.to_string()));
subscription
.remove_candles(&[base], hourly)
.await
.expect("the venue accepted the unsubscribe");
let left: Vec<String> = subscription
.subscribed()
.into_iter()
.map(|(symbol, _)| symbol)
.collect();
assert_eq!(
left,
vec![five_wire.clone()],
"removing one period must leave the other alone"
);
assert!(!subscription.history_loaded(&hourly_wire));
assert!(
subscription.history_loaded(&five_wire),
"the period that was kept keeps its history too"
);
harness.send(flagged_candle(&hourly_wire, 3_000, 0)).await;
harness.send(flagged_candle(&five_wire, 3_000, 0)).await;
let live = market_of(next(&mut mine).await);
assert_eq!(
event_symbol(&live),
Some(five_wire.as_str()),
"the first thing to arrive must be the series that was kept"
);
nothing_more(&mut mine).await;
loop_handle.abort();
}
#[tokio::test]
async fn removing_a_shared_series_does_not_take_it_off_the_wire() {
let harness = Harness::start();
let (_mine, _) = harness.watch(1, "AAPL{=5m}", 16).await;
also_watch(&harness, 1, "MSFT{=5m}").await;
let (_theirs, _) = harness.watch(2, "AAPL{=5m}", 16).await;
let shared = feed_subscriptions(&[candle_target("AAPL{=5m}")]);
assert!(
orphaned_subscriptions(&harness.routing, 1, &shared)
.await
.is_empty(),
"a series another subscription still holds must not leave the wire"
);
let mine_alone = feed_subscriptions(&[candle_target("MSFT{=5m}")]);
assert_eq!(
orphaned_subscriptions(&harness.routing, 1, &mine_alone)
.await
.into_iter()
.map(|request| request.symbol)
.collect::<Vec<_>>(),
vec!["MSFT{=5m}".to_string()],
"a series nobody else holds is the one to take off the wire"
);
let (commands, command_rx) = mpsc::channel::<DXLinkCommand>(8);
let (loop_handle, unsubscribed) =
spawn_recording_command_loop(command_rx, harness.routing.clone());
let subscription = subscription_for(
1,
commands.clone(),
&harness,
shared_targets(&["AAPL{=5m}", "MSFT{=5m}"]),
);
let five = CandlePeriod::minutes(5).expect("a period");
subscription
.remove_candles(&[DxFeedSymbol("AAPL".to_string())], five)
.await
.expect("the removal is accepted");
assert!(
unsubscribed
.lock()
.expect("not poisoned in tests")
.is_empty(),
"a series another subscription still holds must not leave the wire"
);
assert_eq!(
subscription.subscribed(),
vec![("MSFT{=5m}".to_string(), EventKind::Candle)]
);
assert!(
harness
.routing
.read()
.await
.routes
.contains_key(&("AAPL{=5m}".to_string(), EventKind::Candle)),
"the other subscription keeps its route"
);
subscription
.remove_candles(&[DxFeedSymbol("MSFT".to_string())], five)
.await
.expect("the removal is accepted");
assert_eq!(
*unsubscribed.lock().expect("not poisoned in tests"),
vec!["MSFT{=5m}".to_string()],
"a series nobody is left watching is the one to unsubscribe"
);
loop_handle.abort();
}
#[tokio::test]
async fn a_new_snapshot_is_not_ended_by_the_terminator_of_the_last_one() {
let harness = Harness::start();
let (mut rx, _) = harness.watch(1, "AAPL{=5m}", 16).await;
harness
.send(flagged_candle("AAPL{=5m}", 1_000, SNAPSHOT_BEGIN))
.await;
harness
.send(flagged_candle(
"AAPL{=5m}",
2_000,
SNAPSHOT_END | TX_PENDING,
))
.await;
assert_eq!(begin_of(next(&mut rx).await).generation, 1);
assert_eq!(bar_time(next(&mut rx).await), 1_000);
assert_eq!(bar_time(next(&mut rx).await), 2_000);
nothing_more(&mut rx).await;
harness
.send(flagged_candle("AAPL{=5m}", 3_000, SNAPSHOT_BEGIN))
.await;
assert_eq!(bar_time(next(&mut rx).await), 3_000);
nothing_more(&mut rx).await;
assert_eq!(
harness.resume_of(1, "AAPL{=5m}").map(|r| r.phase),
Some(SnapshotPhase::Open),
"the new replay is still running"
);
harness
.send(flagged_candle("AAPL{=5m}", 4_000, SNAPSHOT_END))
.await;
assert_eq!(bar_time(next(&mut rx).await), 4_000);
assert_eq!(end_of(next(&mut rx).await).generation, 1);
}
#[tokio::test]
async fn a_resubscribed_series_does_not_reuse_a_generation() {
let harness = Harness::start();
let (mut rx, _) = harness.watch(1, "AAPL{=5m}", 16).await;
let (commands, command_rx) = mpsc::channel::<DXLinkCommand>(8);
let loop_handle = spawn_routing_command_loop(command_rx, harness.routing.clone());
let targets = shared_targets(&["AAPL{=5m}"]);
let subscription = subscription_for(1, commands, &harness, targets.clone());
let five = CandlePeriod::minutes(5).expect("a period");
harness
.send(flagged_candle("AAPL{=5m}", 1_000, SNAPSHOT_BEGIN))
.await;
harness
.send(flagged_candle("AAPL{=5m}", 2_000, SNAPSHOT_END))
.await;
assert_eq!(begin_of(next(&mut rx).await).generation, 1);
let _ = bar_time(next(&mut rx).await);
let _ = bar_time(next(&mut rx).await);
assert_eq!(end_of(next(&mut rx).await).generation, 1);
subscription
.remove_candles(&[DxFeedSymbol("AAPL".to_string())], five)
.await
.expect("the removal is accepted");
assert!(
!subscription.history_loaded("AAPL{=5m}"),
"a series nobody holds has no loaded history"
);
targets_of(&targets).insert(candle_target("AAPL{=5m}"));
record_routes(
&harness.routing,
1,
&feed_subscriptions(&[candle_target("AAPL{=5m}")]),
)
.await;
harness
.send(flagged_candle("AAPL{=5m}", 5_000, SNAPSHOT_BEGIN))
.await;
assert_eq!(
begin_of(next(&mut rx).await).generation,
2,
"the second replay must be distinguishable from the first"
);
loop_handle.abort();
}
#[tokio::test]
async fn a_cancelled_removal_leaves_the_subscription_intact() {
let harness = Harness::start();
let (commands, _command_rx) = mpsc::channel::<DXLinkCommand>(8);
let subscription = subscription_for(1, commands, &harness, shared_targets(&["AAPL{=5m}"]));
let five = CandlePeriod::minutes(5).expect("a period");
assert!(
tokio::time::timeout(
Duration::from_millis(100),
subscription.remove_candles(&[DxFeedSymbol("AAPL".to_string())], five),
)
.await
.is_err(),
"the removal must still be waiting when it is dropped"
);
assert_eq!(
subscription.subscribed(),
vec![("AAPL{=5m}".to_string(), EventKind::Candle)],
"a removal nobody confirmed must leave the series subscribed, or a \
reconnect stops replaying something the venue is still serving"
);
}
#[tokio::test]
async fn a_reconnect_rebaselines_the_feed_client_loss_counter() {
let harness = Harness::start();
let (mut rx, _) = harness.watch(1, "AAPL{=5m}", 16).await;
harness.dxlink_drops.store(500, Ordering::Relaxed);
harness
.send(flagged_candle("AAPL{=5m}", 1_000, SNAPSHOT_BEGIN))
.await;
let _ = begin_of(next(&mut rx).await);
let _ = bar_time(next(&mut rx).await);
open_generations_for_the_next_connection(
&harness.progress,
&harness.routing,
&harness.history,
&harness.dxlink_drops,
)
.await;
assert_eq!(begin_of(next(&mut rx).await).generation, 2);
harness.dxlink_drops.store(3, Ordering::Relaxed);
harness
.send(flagged_candle("AAPL{=5m}", 2_000, SNAPSHOT_END))
.await;
let _ = bar_time(next(&mut rx).await);
let end = end_of(next(&mut rx).await);
assert_eq!(end.generation, 2);
assert!(
!end.lossless,
"three events were shed during this replay; a baseline from the \
previous connection would have hidden them"
);
}
#[tokio::test]
async fn a_replay_without_a_begin_still_reports_what_it_lost() {
let harness = Harness::start();
let (_rx, lagged) = harness.watch(1, "AAPL{=5m}", 1).await;
for time in [1_000i64, 2_000, 3_000] {
harness.send(flagged_candle("AAPL{=5m}", time, 0)).await;
}
harness
.send(flagged_candle(
"AAPL{=5m}",
4_000,
SNAPSHOT_END | REMOVE_EVENT,
))
.await;
tokio::time::sleep(Duration::from_millis(100)).await;
assert!(lagged.load(Ordering::Relaxed) >= 1, "bars were dropped");
let ended = harness
.resume_of(1, "AAPL{=5m}")
.expect("the series was seen");
assert_eq!(ended.phase, SnapshotPhase::Ended);
assert!(
!ended.lossless,
"a replay the venue never announced still lost bars, and saying \
otherwise reports a history that has holes as complete"
);
}
#[tokio::test]
async fn a_marker_backlog_is_bounded_and_keeps_the_newest() {
let harness = Harness::start();
let (mut rx, _) = harness.watch(1, "AAPL{=5m}", 1).await;
harness
.send(flagged_candle("AAPL{=5m}", 1_000, SNAPSHOT_BEGIN))
.await;
tokio::time::sleep(Duration::from_millis(50)).await;
let reconnects = MAX_PENDING_MARKERS + 20;
for _ in 0..reconnects {
open_generations_for_the_next_connection(
&harness.progress,
&harness.routing,
&harness.history,
&harness.dxlink_drops,
)
.await;
}
let parked = harness
.routing
.read()
.await
.senders
.get(&1)
.expect("the consumer is registered")
.first()
.expect("one channel")
.pending
.lock()
.expect("not poisoned in tests")
.len();
assert!(
parked <= MAX_PENDING_MARKERS,
"the backlog grew past its bound: {parked}"
);
let mut seen = Vec::new();
for _ in 0..=MAX_PENDING_MARKERS {
harness.drained.notify_one();
match tokio::time::timeout(Duration::from_millis(50), rx.recv()).await {
Ok(Some(delivery)) => seen.push(begin_of(delivery).generation),
_ => break,
}
}
let last = *seen.last().expect("something was delivered");
assert_eq!(
last,
(reconnects + 1) as u64,
"the newest phase change must survive the bound"
);
}
#[tokio::test]
async fn a_removal_cancelled_before_it_is_queued_leaves_the_series_intact() {
let harness = Harness::start();
let (commands, _command_rx) = mpsc::channel::<DXLinkCommand>(1);
commands
.try_send(DXLinkCommand::RemoveEventSender(99))
.expect("the one slot is free");
assert!(
commands
.try_send(DXLinkCommand::RemoveEventSender(99))
.is_err(),
"the queue must actually be full for this test to mean anything"
);
let subscription = subscription_for(1, commands, &harness, shared_targets(&["AAPL{=5m}"]));
let five = CandlePeriod::minutes(5).expect("a period");
assert!(
tokio::time::timeout(
Duration::from_millis(100),
subscription.remove_candles(&[DxFeedSymbol("AAPL".to_string())], five),
)
.await
.is_err(),
"the removal must still be waiting to be queued when it is dropped"
);
assert_eq!(
subscription.subscribed(),
vec![("AAPL{=5m}".to_string(), EventKind::Candle)],
"a removal that never reached the command queue must leave the \
series subscribed"
);
}
#[tokio::test]
async fn an_unread_ending_cannot_collide_with_a_later_generation() {
let harness = Harness::start();
let (mut rx, _) = harness.watch(1, "AAPL{=5m}", 16).await;
let (commands, command_rx) = mpsc::channel::<DXLinkCommand>(8);
let loop_handle = spawn_routing_command_loop(command_rx, harness.routing.clone());
let targets = shared_targets(&["AAPL{=5m}"]);
let subscription = subscription_for(1, commands, &harness, targets.clone());
let five = CandlePeriod::minutes(5).expect("a period");
harness
.send(flagged_candle("AAPL{=5m}", 1_000, SNAPSHOT_BEGIN))
.await;
harness
.send(flagged_candle("AAPL{=5m}", 2_000, SNAPSHOT_END))
.await;
tokio::time::sleep(Duration::from_millis(50)).await;
subscription
.remove_candles(&[DxFeedSymbol("AAPL".to_string())], five)
.await
.expect("the removal is accepted");
targets_of(&targets).insert(candle_target("AAPL{=5m}"));
record_routes(
&harness.routing,
1,
&feed_subscriptions(&[candle_target("AAPL{=5m}")]),
)
.await;
harness
.send(flagged_candle("AAPL{=5m}", 5_000, SNAPSHOT_BEGIN))
.await;
assert_eq!(begin_of(next(&mut rx).await).generation, 1);
assert_eq!(bar_time(next(&mut rx).await), 1_000);
assert_eq!(bar_time(next(&mut rx).await), 2_000);
let old_end = end_of(next(&mut rx).await).generation;
let new_begin = begin_of(next(&mut rx).await).generation;
assert_eq!(old_end, 1);
assert_eq!(
new_begin, 2,
"an ending still in the queue must stay distinguishable from the \
replay that replaced it"
);
loop_handle.abort();
}
#[tokio::test]
async fn a_remove_processed_after_a_concurrent_add_leaves_the_new_series_routable() {
let harness = Harness::start();
let (mut mine, _) = harness.watch(1, "AAPL{=5m}", 16).await;
let targets = shared_targets(&["AAPL{=5m}"]);
let (commands, mut command_rx) = mpsc::channel::<DXLinkCommand>(8);
let routing = harness.routing.clone();
let loop_handle = tokio::spawn(async move {
let first = command_rx.recv().await.expect("the remove is queued");
let second = command_rx.recv().await.expect("the add is queued");
assert!(
matches!(first, DXLinkCommand::Unsubscribe(..)),
"the remove reaches the queue first"
);
let mut wire = Vec::new();
for cmd in [second, first] {
match cmd {
DXLinkCommand::Subscribe(requests, _, sub_id, held, ack) => {
assert!(
withdrawn_request(&held, &requests).is_none(),
"the add's target is still reserved"
);
record_routes(&routing, sub_id, &requests).await;
answer(ack, Ok(()));
}
DXLinkCommand::Unsubscribe(requests, sub_id, still_held, ack) => {
let plan =
plan_unsubscribe(&routing, sub_id, still_held.as_deref(), &requests)
.await;
wire.extend(plan.orphaned.into_iter().map(|request| request.symbol));
forget_routes(&routing, sub_id, &plan.released).await;
answer(ack, Ok(()));
}
_ => {}
}
}
wire
});
let subscription = subscription_for(1, commands, &harness, targets.clone());
let five = CandlePeriod::minutes(5).expect("a period");
let later = DateTime::from_timestamp(1_700_000_000, 0).expect("a timestamp");
let symbols = [DxFeedSymbol("AAPL".to_string())];
let (removed, added) = tokio::join!(
subscription.remove_candles(&symbols, five),
subscription.add_candles(&symbols, five, later),
);
removed.expect("the removal is accepted");
added.expect("the re-subscription is accepted");
let wire = loop_handle.await.expect("the stand-in loop finishes");
assert!(
wire.is_empty(),
"the series the add just subscribed must not be taken off the wire: {wire:?}"
);
assert_eq!(
targets_of(&targets).iter().cloned().collect::<Vec<_>>(),
vec![FeedTarget {
kind: EventKind::Candle,
symbol: "AAPL{=5m}".to_string(),
from_time: Some(later.timestamp_millis()),
}],
"only the newer target survives"
);
assert!(
harness
.routing
.read()
.await
.routes
.get(&("AAPL{=5m}".to_string(), EventKind::Candle))
.is_some_and(|holders| holders.contains(&1)),
"the surviving target must still be routable"
);
harness.send(flagged_candle("AAPL{=5m}", 5_000, 0)).await;
assert_eq!(
bar_time(next(&mut mine).await),
5_000,
"a bar for the surviving series reaches the consumer"
);
}
#[tokio::test]
async fn removing_the_only_target_of_a_series_still_releases_it() {
let harness = Harness::start();
let (mut mine, _) = harness.watch(1, "AAPL{=5m}", 16).await;
also_watch(&harness, 1, "AAPL{=h}").await;
let (commands, command_rx) = mpsc::channel::<DXLinkCommand>(8);
let (loop_handle, unsubscribed) =
spawn_recording_command_loop(command_rx, harness.routing.clone());
let targets = shared_targets(&["AAPL{=5m}", "AAPL{=h}"]);
let subscription = subscription_for(1, commands, &harness, targets.clone());
let five = CandlePeriod::minutes(5).expect("a period");
subscription
.remove_candles(&[DxFeedSymbol("AAPL".to_string())], five)
.await
.expect("the removal is accepted");
assert_eq!(
unsubscribed.lock().expect("not poisoned in tests").clone(),
vec!["AAPL{=5m}".to_string()],
"the last holder's series leaves the wire"
);
assert!(
!harness
.routing
.read()
.await
.routes
.contains_key(&("AAPL{=5m}".to_string(), EventKind::Candle)),
"the route goes with the only target"
);
assert_eq!(
subscription.subscribed(),
vec![("AAPL{=h}".to_string(), EventKind::Candle)],
"the hourly series is untouched"
);
harness.send(flagged_candle("AAPL{=5m}", 5_000, 0)).await;
nothing_more(&mut mine).await;
loop_handle.abort();
}
#[tokio::test]
async fn a_plan_releases_only_what_the_subscription_no_longer_holds() {
let routing: Arc<RwLock<EventRouting>> = Arc::new(RwLock::new(EventRouting::default()));
let requested = feed_subscriptions(&[candle_target("AAPL{=5m}")]);
record_routes(&routing, 1, &requested).await;
let held = Arc::new(Mutex::new(BTreeSet::from([FeedTarget {
from_time: Some(99),
..candle_target("AAPL{=5m}")
}])));
let plan = plan_unsubscribe(&routing, 1, Some(&held), &requested).await;
assert!(plan.released.is_empty(), "the series is still wanted");
assert!(plan.orphaned.is_empty(), "so the venue hears nothing");
let other_period = shared_targets(&["AAPL{=h}"]);
let plan = plan_unsubscribe(&routing, 1, Some(&other_period), &requested).await;
assert_eq!(plan.released.len(), 1, "the five-minute series is released");
assert_eq!(plan.orphaned.len(), 1, "and nobody else holds it");
let plan = plan_unsubscribe(&routing, 1, None, &requested).await;
assert_eq!(plan.released.len(), 1);
assert_eq!(plan.orphaned.len(), 1);
record_routes(&routing, 2, &requested).await;
let plan = plan_unsubscribe(&routing, 1, None, &requested).await;
assert_eq!(plan.released.len(), 1);
assert!(plan.orphaned.is_empty(), "somebody else is still watching");
}
#[tokio::test]
async fn a_subscription_without_a_sender_is_closed_to_the_loop() {
let harness = Harness::start();
let (_mine, _) = harness.watch(1, "AAPL{=5m}", 4).await;
assert!(subscription_is_open(&harness.routing, 1).await);
assert!(!subscription_is_open(&harness.routing, 2).await);
harness.routing.write().await.senders.remove(&1);
assert!(
!subscription_is_open(&harness.routing, 1).await,
"once the sender is gone, so is the subscription"
);
}
#[tokio::test]
async fn a_subscribe_whose_target_was_withdrawn_is_refused() {
let harness = Harness::start();
let (_mine, _) = harness.watch(1, "MSFT{=5m}", 4).await;
let (commands, command_rx) = mpsc::channel::<DXLinkCommand>(8);
let (loop_handle, unsubscribed) =
spawn_recording_command_loop(command_rx, harness.routing.clone());
let targets = shared_targets(&["MSFT{=5m}"]);
let subscription = subscription_for(1, commands, &harness, targets.clone());
let five = CandlePeriod::minutes(5).expect("a period");
let start = DateTime::from_timestamp(1_700_000_000, 0).expect("a timestamp");
let symbols = [DxFeedSymbol("AAPL".to_string())];
let (added, ()) = tokio::join!(subscription.add_candles(&symbols, five, start), async {
targets_of(&targets).retain(|target| target.symbol != "AAPL{=5m}");
});
let error = added.expect_err("a withdrawn target is not subscribed");
assert!(
error.to_string().contains("AAPL{=5m}"),
"the caller is told which series went missing: {error}"
);
assert!(
!harness
.routing
.read()
.await
.routes
.contains_key(&("AAPL{=5m}".to_string(), EventKind::Candle)),
"no route for a target nobody holds"
);
assert_eq!(
subscription.subscribed(),
vec![("MSFT{=5m}".to_string(), EventKind::Candle)],
"the reservation was given back and the other series is untouched"
);
assert!(
unsubscribed
.lock()
.expect("not poisoned in tests")
.is_empty(),
"nothing reached the venue"
);
loop_handle.abort();
}
#[tokio::test]
async fn a_request_without_a_route_is_not_sent_to_the_venue() {
let routing: Arc<RwLock<EventRouting>> = Arc::new(RwLock::new(EventRouting::default()));
let never_landed = feed_subscriptions(&[candle_target("AAPL{=5m}")]);
assert!(
orphaned_subscriptions(&routing, 1, &never_landed)
.await
.is_empty(),
"a series with no route is not on the wire for anybody"
);
}
#[tokio::test]
async fn a_close_refuses_a_target_added_after_it_began() {
let harness = Harness::start();
let (commands, command_rx) = mpsc::channel::<DXLinkCommand>(8);
let (shutdown_tx, _shutdown_rx) = oneshot::channel::<()>();
let (loop_handle, unsubscribed) =
spawn_recording_command_loop(command_rx, harness.routing.clone());
let mut streamer = streamer_with(commands, shutdown_tx);
let subscription = streamer
.create_sub([EventKind::Candle])
.await
.expect("the streamer is open");
let id = subscription.id;
let five = CandlePeriod::minutes(5).expect("a period");
let hourly = CandlePeriod::hours(1).expect("a period");
let start = DateTime::from_timestamp(1_700_000_000, 0).expect("a timestamp");
let symbols = [DxFeedSymbol("AAPL".to_string())];
subscription
.add_candles(&symbols, five, start)
.await
.expect("subscribing succeeds");
let (closed, added) = tokio::join!(
streamer.close_sub(id),
subscription.add_candles(&symbols, hourly, start),
);
closed.expect("the close is accepted");
let error = added.expect_err("a closed subscription accepts nothing");
assert!(error.to_string().contains("closed"), "{error}");
assert_eq!(
unsubscribed.lock().expect("not poisoned in tests").clone(),
vec!["AAPL{=5m}".to_string()],
"exactly what was subscribed leaves the wire"
);
assert!(subscription.subscribed().is_empty());
assert!(
harness.routing.read().await.routes.is_empty(),
"nothing routes to a closed subscription"
);
assert!(streamer.get_sub(id).is_none());
loop_handle.abort();
}
#[tokio::test]
async fn a_close_beats_a_subscribe_that_reserved_before_it() {
let harness = Harness::start();
let (commands, command_rx) = mpsc::channel::<DXLinkCommand>(8);
let (shutdown_tx, _shutdown_rx) = oneshot::channel::<()>();
let (loop_handle, unsubscribed) =
spawn_recording_command_loop(command_rx, harness.routing.clone());
let mut streamer = streamer_with(commands, shutdown_tx);
let subscription = streamer
.create_sub([EventKind::Candle])
.await
.expect("the streamer is open");
let id = subscription.id;
let five = CandlePeriod::minutes(5).expect("a period");
let hourly = CandlePeriod::hours(1).expect("a period");
let start = DateTime::from_timestamp(1_700_000_000, 0).expect("a timestamp");
let symbols = [DxFeedSymbol("AAPL".to_string())];
subscription
.add_candles(&symbols, five, start)
.await
.expect("subscribing succeeds");
let (added, closed) = tokio::join!(
subscription.add_candles(&symbols, hourly, start),
streamer.close_sub(id),
);
closed.expect("the close is accepted");
let error = added.expect_err("the late subscribe is refused");
assert!(error.to_string().contains("AAPL{=h}"), "{error}");
assert_eq!(
unsubscribed.lock().expect("not poisoned in tests").clone(),
vec!["AAPL{=5m}".to_string()],
"the hourly series never landed, so the venue is not told to stop it"
);
assert!(subscription.subscribed().is_empty());
assert!(harness.routing.read().await.routes.is_empty());
loop_handle.abort();
}
#[tokio::test]
async fn a_refused_close_reopens_the_subscription() {
let (commands, mut command_rx) = mpsc::channel::<DXLinkCommand>(8);
let (shutdown_tx, _shutdown_rx) = oneshot::channel::<()>();
let loop_handle = tokio::spawn(async move {
while let Some(cmd) = command_rx.recv().await {
match cmd {
DXLinkCommand::Subscribe(_, _, _, _, ack) => answer(ack, Ok(())),
DXLinkCommand::Unsubscribe(_, _, _, ack) => answer(
ack,
Err(TastyTradeError::Streaming("the venue refused".to_string())),
),
_ => {}
}
}
});
let mut streamer = streamer_with(commands, shutdown_tx);
let subscription = streamer
.create_sub([EventKind::Candle])
.await
.expect("the streamer is open");
let id = subscription.id;
let five = CandlePeriod::minutes(5).expect("a period");
let hourly = CandlePeriod::hours(1).expect("a period");
let start = DateTime::from_timestamp(1_700_000_000, 0).expect("a timestamp");
let symbols = [DxFeedSymbol("AAPL".to_string())];
subscription
.add_candles(&symbols, five, start)
.await
.expect("subscribing succeeds");
streamer
.close_sub(id)
.await
.expect_err("the venue refused the unsubscribe");
assert_eq!(
subscription.subscribed(),
vec![("AAPL{=5m}".to_string(), EventKind::Candle)],
"the record a retry needs is still there"
);
subscription
.add_candles(&symbols, hourly, start)
.await
.expect("the subscription is open again");
assert!(
streamer.get_sub(id).is_some(),
"the subscription is still there"
);
loop_handle.abort();
}
fn poll_once<F: std::future::Future>(
future: std::pin::Pin<&mut F>,
) -> std::task::Poll<F::Output> {
let mut cx = std::task::Context::from_waker(std::task::Waker::noop());
future.poll(&mut cx)
}
#[tokio::test]
async fn a_late_removal_does_not_erase_a_finished_replacement_replay() {
let harness = Harness::start();
let (mut mine, _) = harness.watch(1, "AAPL{=5m}", 16).await;
let (commands, command_rx) = mpsc::channel::<DXLinkCommand>(8);
let (loop_handle, unsubscribed) =
spawn_recording_command_loop(command_rx, harness.routing.clone());
let targets = shared_targets(&["AAPL{=5m}"]);
let subscription = subscription_for(1, commands, &harness, targets.clone());
let five = CandlePeriod::minutes(5).expect("a period");
let later = DateTime::from_timestamp(1_700_000_000, 0).expect("a timestamp");
let symbols = [DxFeedSymbol("AAPL".to_string())];
let removal = subscription.remove_candles(&symbols, five);
tokio::pin!(removal);
assert!(poll_once(removal.as_mut()).is_pending());
let released = async {
while unsubscribed
.lock()
.expect("not poisoned in tests")
.is_empty()
{
tokio::task::yield_now().await;
}
};
tokio::time::timeout(Duration::from_secs(2), released)
.await
.expect("the loop processes the removal");
subscription
.add_candles(&symbols, five, later)
.await
.expect("re-subscribing succeeds");
harness
.send(flagged_candle("AAPL{=5m}", 1_000, SNAPSHOT_BEGIN))
.await;
harness
.send(flagged_candle("AAPL{=5m}", 2_000, SNAPSHOT_END))
.await;
begin_of(next(&mut mine).await);
assert_eq!(bar_time(next(&mut mine).await), 1_000);
assert_eq!(bar_time(next(&mut mine).await), 2_000);
end_of(next(&mut mine).await);
assert!(subscription.history_loaded("AAPL{=5m}"));
removal.await.expect("the removal was accepted");
assert!(
subscription.history_loaded("AAPL{=5m}"),
"a finished replacement replay must survive an older removal's cleanup"
);
assert_eq!(
subscription.subscribed(),
vec![("AAPL{=5m}".to_string(), EventKind::Candle)]
);
loop_handle.abort();
}
}