use std::{
collections::HashMap,
fmt::Debug,
ops::Deref,
sync::{Arc, LazyLock, Mutex},
time::Duration,
};
use anyhow::Context;
use ibapi::client::Client;
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
struct ConnectionKey(String, u16, i32);
type RegistryMap = HashMap<ConnectionKey, (Arc<Client>, u32)>;
static REGISTRY: LazyLock<Arc<Mutex<RegistryMap>>> =
LazyLock::new(|| Arc::new(Mutex::new(HashMap::new())));
pub struct SharedClientHandle {
client: Arc<Client>,
registry: Arc<Mutex<RegistryMap>>,
key: ConnectionKey,
}
impl Debug for SharedClientHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct(stringify!(SharedClientHandle))
.field("key", &self.key)
.finish_non_exhaustive()
}
}
impl SharedClientHandle {
fn new(client: Arc<Client>, registry: Arc<Mutex<RegistryMap>>, key: ConnectionKey) -> Self {
Self {
client,
registry,
key,
}
}
pub fn as_arc(&self) -> &Arc<Client> {
&self.client
}
}
impl Deref for SharedClientHandle {
type Target = Client;
fn deref(&self) -> &Self::Target {
self.client.as_ref()
}
}
impl Drop for SharedClientHandle {
fn drop(&mut self) {
if let Ok(mut guard) = self.registry.lock()
&& let Some((_, ref_count)) = guard.get_mut(&self.key)
{
*ref_count = ref_count.saturating_sub(1);
if *ref_count == 0 {
guard.remove(&self.key);
tracing::debug!(
"Shared IB client removed from registry (host={}, port={}, client_id={})",
self.key.0,
self.key.1,
self.key.2
);
}
}
}
}
pub async fn get_or_connect(
host: &str,
port: u16,
client_id: i32,
connection_timeout_secs: u64,
) -> anyhow::Result<SharedClientHandle> {
let key = ConnectionKey(host.to_string(), port, client_id);
let registry = Arc::clone(®ISTRY);
log::debug!(
"Acquiring shared IB client (host={}, port={}, client_id={}, timeout_secs={})",
host,
port,
client_id,
connection_timeout_secs
);
let (reuse_client, ref_count_val) = {
let mut guard = registry
.lock()
.map_err(|e| anyhow::anyhow!("Registry mutex poisoned: {e}"))?;
if let Some((client, ref_count)) = guard.get_mut(&key) {
if client.is_connected() {
*ref_count += 1;
let ref_count_val = *ref_count;
let client = Arc::clone(client);
(Some(client), ref_count_val)
} else {
tracing::debug!(
"Removing disconnected shared IB client before reconnect (host={}, port={}, client_id={})",
host,
port,
client_id
);
guard.remove(&key);
(None, 0)
}
} else {
(None, 0)
}
};
if let Some(client) = reuse_client {
log::debug!(
"Reusing shared IB client (host={}, port={}, client_id={}, ref_count={})",
host,
port,
client_id,
ref_count_val
);
return Ok(SharedClientHandle::new(client, registry, key));
}
let address = format!("{host}:{port}");
let connect_timeout = Duration::from_secs(connection_timeout_secs);
log::debug!(
"No shared IB client found, establishing new connection to {} with timeout {:?}",
address,
connect_timeout
);
let client = tokio::time::timeout(connect_timeout, Client::connect(&address, client_id))
.await
.map_err(|_| {
anyhow::anyhow!(
"Timed out connecting to IB Gateway/TWS after {}s",
connection_timeout_secs
)
})?
.context("Failed to connect to IB Gateway/TWS")?;
let client = Arc::new(client);
{
let mut guard = registry
.lock()
.map_err(|e| anyhow::anyhow!("Registry mutex poisoned: {e}"))?;
log::debug!(
"Registering shared IB client in registry (host={}, port={}, client_id={})",
host,
port,
client_id
);
guard.insert(key.clone(), (Arc::clone(&client), 1));
}
tracing::debug!(
"Registered new shared IB client (host={}, port={}, client_id={})",
host,
port,
client_id
);
Ok(SharedClientHandle::new(client, registry, key))
}