use std::{
collections::HashSet,
sync::{
atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering},
LazyLock, Mutex, RwLock,
},
time::Duration,
};
#[cfg(feature = "sync")]
use crossbeam::channel;
use crate::accounts::types::AccountId;
use crate::client::ids::{OrderId, RequestId};
use crate::messages::{OutgoingMessages, ResponseMessage};
use crate::transport::common::Lease;
use crate::transport::routing::{classify_error, determine_routing, ErrorDisposition, RoutingDecision};
use crate::transport::{RoutedItem, SharedTicket};
use crate::Error;
#[cfg(feature = "sync")]
use crate::transport::{InternalSubscription, MessageBus, Signal, SubscriptionBuilder};
#[cfg(feature = "async")]
use {
crate::transport::{
r#async::{AsyncInternalSubscription, CleanupSignal, RouteKey},
AsyncMessageBus,
},
async_trait::async_trait,
tokio::sync::broadcast,
};
#[cfg(feature = "async")]
const TEST_BROADCAST_CAPACITY: usize = 1024;
pub(crate) struct MessageBusStub {
pub request_messages: RwLock<Vec<Vec<u8>>>,
pub ordered_responses: Vec<ResponseMessage>,
pub buffer_limits: RwLock<Vec<usize>>,
#[cfg(feature = "async")]
pub executions_requests: RwLock<Vec<RequestId>>,
connection_resets: AtomicUsize,
session_generation: AtomicU64,
#[cfg(feature = "async")]
runtime: Option<tokio::runtime::Handle>,
#[cfg(feature = "sync")]
signals: (channel::Sender<Signal>, channel::Receiver<Signal>),
#[cfg(feature = "async")]
status_senders: Mutex<Vec<broadcast::Sender<RoutedItem>>>,
keep_open: AtomicBool,
reconnect_delay: Duration,
#[cfg(feature = "sync")]
open_sync_senders: Mutex<Vec<channel::Sender<RoutedItem>>>,
#[cfg(feature = "async")]
open_async_senders: Mutex<Vec<broadcast::Sender<RoutedItem>>>,
}
static ORDER_UPDATE_SUBSCRIPTION_TRACKER: LazyLock<Mutex<HashSet<usize>>> = LazyLock::new(|| Mutex::new(HashSet::new()));
impl MessageBusStub {
pub(crate) fn start_new_session(&self) {
self.session_generation.fetch_add(1, Ordering::Relaxed);
}
}
impl Default for MessageBusStub {
fn default() -> Self {
Self {
request_messages: RwLock::new(vec![]),
ordered_responses: vec![],
buffer_limits: RwLock::new(vec![]),
#[cfg(feature = "async")]
executions_requests: RwLock::new(vec![]),
connection_resets: AtomicUsize::new(0),
session_generation: AtomicU64::new(0),
#[cfg(feature = "async")]
runtime: tokio::runtime::Handle::try_current().ok(),
#[cfg(feature = "sync")]
signals: channel::unbounded(),
#[cfg(feature = "async")]
status_senders: Mutex::default(),
keep_open: AtomicBool::new(false),
reconnect_delay: Duration::ZERO,
#[cfg(feature = "sync")]
open_sync_senders: Mutex::default(),
#[cfg(feature = "async")]
open_async_senders: Mutex::default(),
}
}
}
impl Drop for MessageBusStub {
fn drop(&mut self) {
let stub_id = self as *const _ as usize;
ORDER_UPDATE_SUBSCRIPTION_TRACKER.lock().unwrap().remove(&stub_id);
}
}
impl MessageBusStub {
fn record_buffer_limit(&self, bound: Option<crate::transport::BufferBound>) {
if let Some(bound) = bound {
self.buffer_limits.write().unwrap().push(bound.limit);
}
}
pub fn with_ordered_responses(ordered_responses: Vec<ResponseMessage>) -> Self {
let mut stub = Self::default();
stub.ordered_responses = ordered_responses;
stub
}
pub fn with_connection_resets(self, count: usize) -> Self {
self.connection_resets.store(count, Ordering::SeqCst);
self
}
pub fn keeping_channels_open(self) -> Self {
self.keep_open.store(true, Ordering::SeqCst);
self
}
pub fn with_reconnect_delay(mut self, delay: Duration) -> Self {
self.reconnect_delay = delay;
self
}
pub fn request_messages(&self) -> Vec<Vec<u8>> {
self.request_messages.read().unwrap().clone()
}
pub(crate) fn routed_items(&self) -> Vec<RoutedItem> {
self.ordered_responses.iter().cloned().map(classify_like_dispatcher).collect()
}
fn routed_items_for_request(&self) -> Vec<RoutedItem> {
let remaining = self.connection_resets.load(Ordering::SeqCst);
if remaining > 0 {
self.connection_resets.store(remaining - 1, Ordering::SeqCst);
return vec![RoutedItem::Error(Error::ConnectionReset)];
}
self.routed_items()
}
#[cfg(feature = "async")]
fn seeded_subscription(&self, message: Vec<u8>) -> AsyncInternalSubscription {
self.request_messages.write().unwrap().push(message);
let (sender, receiver) = broadcast::channel(TEST_BROADCAST_CAPACITY);
for item in self.routed_items_for_request() {
sender.send(item).unwrap();
}
if self.keep_open.load(Ordering::SeqCst) {
self.open_async_senders.lock().unwrap().push(sender.clone());
}
AsyncInternalSubscription::new(receiver).in_session(self.session_generation.load(Ordering::Relaxed))
}
}
fn classify_like_dispatcher(message: ResponseMessage) -> RoutedItem {
match determine_routing(&message) {
RoutingDecision::Error(payload) => match classify_error(payload) {
ErrorDisposition::Route(_, item) => item,
ErrorDisposition::NoticeOnly(notice) => RoutedItem::Notice(notice),
ErrorDisposition::NoticeAndFailOneShots(_, error) => RoutedItem::Error(error),
},
RoutingDecision::Shutdown => RoutedItem::Error(Error::Shutdown),
_ => message.into(),
}
}
#[cfg(feature = "sync")]
impl MessageBus for MessageBusStub {
fn send_request_capped(
&self,
request_id: RequestId,
message: &[u8],
bound: Option<crate::transport::BufferBound>,
) -> Result<InternalSubscription, Error> {
self.record_buffer_limit(bound);
Ok(mock_request(self, MockRoute::Request(request_id), message))
}
fn send_order_request(&self, order_id: OrderId, message: &[u8]) -> Result<InternalSubscription, Error> {
Ok(mock_request(self, MockRoute::Order(order_id), message))
}
fn send_message(&self, message: &[u8]) -> Result<(), Error> {
self.request_messages.write().unwrap().push(message.to_vec());
Ok(())
}
fn send_market_data_type(&self, message: &[u8]) -> Result<(), Error> {
self.request_messages.write().unwrap().push(message.to_vec());
Ok(())
}
fn create_order_status_subscription(&self, order_id: OrderId) -> Result<InternalSubscription, Error> {
let (sender, receiver) = channel::unbounded();
for item in self.routed_items_for_request() {
sender.send(item).unwrap();
}
Ok(SubscriptionBuilder::new()
.receiver(receiver)
.sender(sender)
.signaler(self.signals.0.clone())
.lease(Lease::new())
.order_status(order_id)
.build())
}
fn create_order_update_subscription(&self) -> Result<InternalSubscription, Error> {
let stub_id = self as *const _ as usize;
let mut tracker = ORDER_UPDATE_SUBSCRIPTION_TRACKER.lock().unwrap();
if !tracker.insert(stub_id) {
return Err(Error::AlreadySubscribed);
}
drop(tracker);
let (sender, receiver) = channel::unbounded();
for item in self.routed_items() {
sender.send(item).unwrap();
}
let subscription = SubscriptionBuilder::new()
.receiver(receiver)
.signaler(self.signals.0.clone())
.lease(Lease::new())
.build();
Ok(subscription)
}
fn send_shared_request(&self, message_type: OutgoingMessages, message: &[u8]) -> Result<InternalSubscription, Error> {
Ok(mock_request(self, MockRoute::Shared(message_type), message))
}
fn send_account_updates_request(&self, _account: &AccountId, message: &[u8]) -> Result<InternalSubscription, Error> {
MessageBus::send_shared_request(self, OutgoingMessages::RequestAccountData, message)
}
fn cancel_shared_subscription(&self, _ticket: SharedTicket, packet: Option<&[u8]>) -> Result<(), Error> {
if let Some(packet) = packet {
self.request_messages.write().unwrap().push(packet.to_vec());
}
Ok(())
}
fn notice_subscribe(&self) -> crate::subscriptions::notice_stream::sync_impl::NoticeStream {
let (_sender, receiver) = channel::unbounded();
crate::subscriptions::notice_stream::sync_impl::NoticeStream::new(receiver)
}
fn ensure_shutdown(&self) {}
fn wait_connected(&self) -> Result<(), Error> {
if !self.reconnect_delay.is_zero() {
std::thread::sleep(self.reconnect_delay);
}
Ok(())
}
fn is_connected(&self) -> bool {
true }
fn session_generation(&self) -> u64 {
self.session_generation.load(Ordering::Relaxed)
}
}
#[cfg(feature = "sync")]
enum MockRoute {
Request(RequestId),
Order(OrderId),
Shared(OutgoingMessages),
}
#[cfg(feature = "sync")]
fn mock_request(stub: &MessageBusStub, route: MockRoute, message: &[u8]) -> InternalSubscription {
stub.request_messages.write().unwrap().push(message.to_vec());
let (sender, receiver) = channel::unbounded();
for item in stub.routed_items_for_request() {
sender.send(item).unwrap();
}
if stub.keep_open.load(Ordering::SeqCst) {
stub.open_sync_senders.lock().unwrap().push(sender.clone());
}
let subscription = SubscriptionBuilder::new()
.signaler(stub.signals.0.clone())
.lease(Lease::new())
.receiver(receiver)
.session(stub.session_generation.load(Ordering::Relaxed));
match route {
MockRoute::Request(request_id) => subscription.request_id(request_id),
MockRoute::Order(order_id) => subscription.order_id(order_id),
MockRoute::Shared(message_type) => subscription.shared(SharedTicket { message_type, generation: 0 }),
}
.build()
}
#[cfg(feature = "async")]
#[async_trait]
impl AsyncMessageBus for MessageBusStub {
async fn send_request_capped(
&self,
_request_id: RequestId,
message: Vec<u8>,
bound: Option<crate::transport::BufferBound>,
) -> Result<AsyncInternalSubscription, Error> {
self.record_buffer_limit(bound);
Ok(self.seeded_subscription(message))
}
async fn send_executions_request(&self, request_id: RequestId, message: Vec<u8>) -> Result<AsyncInternalSubscription, Error> {
self.executions_requests.write().unwrap().push(request_id);
Ok(self.seeded_subscription(message))
}
async fn send_order_request(&self, _order_id: OrderId, message: Vec<u8>) -> Result<AsyncInternalSubscription, Error> {
Ok(self.seeded_subscription(message))
}
async fn send_shared_request(&self, _message_type: OutgoingMessages, message: Vec<u8>) -> Result<AsyncInternalSubscription, Error> {
Ok(self.seeded_subscription(message))
}
async fn send_account_updates_request(&self, _account: &AccountId, message: Vec<u8>) -> Result<AsyncInternalSubscription, Error> {
AsyncMessageBus::send_shared_request(self, OutgoingMessages::RequestAccountData, message).await
}
async fn send_message(&self, message: Vec<u8>) -> Result<(), Error> {
self.request_messages.write().unwrap().push(message);
Ok(())
}
async fn send_market_data_type(&self, message: Vec<u8>) -> Result<(), Error> {
self.request_messages.write().unwrap().push(message);
Ok(())
}
async fn cancel_shared_subscription(&self, _ticket: SharedTicket, message: Option<Vec<u8>>) -> Result<(), Error> {
if let Some(message) = message {
self.request_messages.write().unwrap().push(message);
}
Ok(())
}
async fn create_order_status_subscription(&self, _order_id: OrderId) -> Result<AsyncInternalSubscription, Error> {
let (sender, receiver) = broadcast::channel(TEST_BROADCAST_CAPACITY);
for item in self.routed_items_for_request() {
sender.send(item).unwrap();
}
self.status_senders.lock().unwrap().push(sender);
Ok(AsyncInternalSubscription::new(receiver))
}
async fn create_order_update_subscription(&self) -> Result<AsyncInternalSubscription, Error> {
let stub_id = self as *const _ as usize;
let mut tracker = ORDER_UPDATE_SUBSCRIPTION_TRACKER.lock().unwrap();
if !tracker.insert(stub_id) {
return Err(Error::AlreadySubscribed);
}
drop(tracker);
let (sender, receiver) = broadcast::channel(TEST_BROADCAST_CAPACITY);
for item in self.routed_items() {
sender.send(item).unwrap();
}
let (cleanup_sender, mut cleanup_receiver) = tokio::sync::mpsc::unbounded_channel();
tokio::spawn(async move {
while let Some(signal) = cleanup_receiver.recv().await {
if matches!(signal, CleanupSignal::OrderUpdateStream(_)) {
ORDER_UPDATE_SUBSCRIPTION_TRACKER.lock().unwrap().remove(&stub_id);
break;
}
}
});
Ok(AsyncInternalSubscription::with_cleanup(receiver, cleanup_sender).leased(Lease::new(), RouteKey::OrderUpdateStream))
}
fn notice_subscribe(&self) -> crate::subscriptions::notice_stream::async_impl::NoticeStream {
let (_sender, receiver) = broadcast::channel(1);
crate::subscriptions::notice_stream::async_impl::NoticeStream::new(receiver)
}
async fn ensure_shutdown(&self) {
}
fn request_shutdown_sync(&self) {
}
async fn wait_connected(&self) -> Result<(), Error> {
if !self.reconnect_delay.is_zero() {
tokio::time::sleep(self.reconnect_delay).await;
}
Ok(())
}
fn is_connected(&self) -> bool {
true }
fn session_generation(&self) -> u64 {
self.session_generation.load(Ordering::Relaxed)
}
fn runtime_handle(&self) -> &tokio::runtime::Handle {
self.runtime.as_ref().expect("MessageBusStub built outside a Tokio runtime")
}
}
#[cfg(test)]
#[path = "stubs_tests.rs"]
mod tests;