use std::{
fmt::Debug,
sync::{
Arc,
atomic::{AtomicBool, AtomicU32, Ordering},
},
time::Duration,
};
use anyhow::Context;
use ibapi::client::Client;
use nautilus_common::live::get_runtime;
#[derive(Debug, Clone)]
pub struct ConnectionManager {
host: String,
port: u16,
client_id: i32,
is_connected: Arc<AtomicBool>,
attempt_count: Arc<AtomicU32>,
max_attempts: u32,
retry_indefinitely: bool,
current_backoff: Arc<std::sync::Mutex<Duration>>,
last_disconnection: Arc<std::sync::Mutex<Option<tokio::time::Instant>>>,
}
impl ConnectionManager {
pub fn new(host: String, port: u16, client_id: i32, max_attempts: u32) -> Self {
Self {
host,
port,
client_id,
is_connected: Arc::new(AtomicBool::new(false)),
attempt_count: Arc::new(AtomicU32::new(0)),
max_attempts,
retry_indefinitely: max_attempts == 0,
current_backoff: Arc::new(std::sync::Mutex::new(Duration::from_secs(1))),
last_disconnection: Arc::new(std::sync::Mutex::new(None)),
}
}
pub async fn connect_with_retry(&self) -> anyhow::Result<Arc<Client>> {
const MAX_BACKOFF: Duration = Duration::from_secs(60);
let mut attempt = 0;
let mut backoff = Duration::from_secs(1);
loop {
attempt += 1;
self.attempt_count.store(attempt, Ordering::Relaxed);
if !self.retry_indefinitely && attempt > self.max_attempts {
anyhow::bail!("Failed to connect after {} attempts", self.max_attempts);
}
tracing::debug!(
"Connection attempt {} to {}:{} (client_id: {})",
attempt,
self.host,
self.port,
self.client_id
);
let address = format!("{}:{}", self.host, self.port);
match Client::connect(&address, self.client_id).await {
Ok(client) => {
tracing::info!(
"Successfully connected to IB Gateway/TWS at {} (client_id: {})",
address,
self.client_id
);
self.is_connected.store(true, Ordering::Relaxed);
self.attempt_count.store(0, Ordering::Relaxed);
*self.current_backoff.lock().unwrap() = Duration::from_secs(1);
return Ok(Arc::new(client));
}
Err(e) => {
tracing::warn!(
"Connection attempt {} failed: {} (backoff: {:?})",
attempt,
e,
backoff
);
if !self.retry_indefinitely && attempt >= self.max_attempts {
return Err(e).context(format!(
"Failed to connect after {} attempts",
self.max_attempts
));
}
tokio::time::sleep(backoff).await;
backoff = std::cmp::min(backoff * 2, MAX_BACKOFF);
*self.current_backoff.lock().unwrap() = backoff;
}
}
}
}
pub fn is_connected(&self) -> bool {
self.is_connected.load(Ordering::Relaxed)
}
pub fn mark_disconnected(&self) {
self.is_connected.store(false, Ordering::Relaxed);
*self.last_disconnection.lock().unwrap() = Some(tokio::time::Instant::now());
}
pub fn attempt_count(&self) -> u32 {
self.attempt_count.load(Ordering::Relaxed)
}
pub fn current_backoff(&self) -> Duration {
*self.current_backoff.lock().unwrap()
}
pub fn time_since_disconnection(&self) -> Option<Duration> {
self.last_disconnection
.lock()
.unwrap()
.map(|time: tokio::time::Instant| time.elapsed())
}
}
#[derive(Clone)]
pub struct ConnectionWatchdog {
manager: Arc<ConnectionManager>,
check_interval: Duration,
client: Arc<std::sync::Mutex<Option<Arc<Client>>>>,
reconnect_callback: Arc<
dyn Fn() -> tokio::task::JoinHandle<anyhow::Result<Arc<Client>>> + Send + Sync + 'static,
>,
is_running: Arc<AtomicBool>,
}
impl Debug for ConnectionWatchdog {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct(stringify!(ConnectionWatchdog))
.field("check_interval", &self.check_interval)
.field("is_running", &self.is_running.load(Ordering::Relaxed))
.finish_non_exhaustive()
}
}
impl ConnectionWatchdog {
pub fn new(
manager: Arc<ConnectionManager>,
check_interval: Duration,
reconnect_callback: Arc<
dyn Fn() -> tokio::task::JoinHandle<anyhow::Result<Arc<Client>>> + Send + Sync,
>,
) -> Self {
Self {
manager,
check_interval,
client: Arc::new(std::sync::Mutex::new(None)),
reconnect_callback,
is_running: Arc::new(AtomicBool::new(false)),
}
}
pub fn set_client(&self, client: Arc<Client>) {
*self.client.lock().unwrap() = Some(client);
}
pub fn start(&self) -> tokio::task::JoinHandle<()> {
let manager = Arc::clone(&self.manager);
let client = Arc::clone(&self.client);
let reconnect_callback = Arc::clone(&self.reconnect_callback);
let check_interval = self.check_interval;
let is_running = Arc::clone(&self.is_running);
is_running.store(true, Ordering::Relaxed);
get_runtime().spawn(async move {
tracing::debug!("Connection watchdog started");
while is_running.load(Ordering::Relaxed) {
tokio::time::sleep(check_interval).await;
if !manager.is_connected() {
tracing::warn!(
"Connection watchdog detected disconnection, triggering reconnection"
);
let handle = reconnect_callback();
match handle.await {
Ok(Ok(new_client)) => {
tracing::info!("Reconnection successful via watchdog");
*client.lock().unwrap() = Some(new_client);
manager.is_connected.store(true, Ordering::Relaxed);
}
Ok(Err(e)) => {
tracing::error!("Reconnection failed via watchdog: {}", e);
}
Err(e) => {
tracing::error!("Reconnection task panicked: {}", e);
}
}
}
}
tracing::debug!("Connection watchdog stopped");
})
}
pub fn stop(&self) {
self.is_running.store(false, Ordering::Relaxed);
}
}