nautilus_interactive_brokers/common/
shared_client.rs1use std::{
23 collections::HashMap,
24 fmt::Debug,
25 ops::Deref,
26 sync::{Arc, LazyLock},
27 time::Duration,
28};
29
30use anyhow::Context;
31use ibapi::client::Client;
32use parking_lot::Mutex;
33
34#[derive(Clone, Debug, Eq, Hash, PartialEq)]
36struct ConnectionKey(String, u16, i32);
37
38type RegistryMap = HashMap<ConnectionKey, (Arc<Client>, u32)>;
40
41static REGISTRY: LazyLock<Arc<Mutex<RegistryMap>>> =
43 LazyLock::new(|| Arc::new(Mutex::new(HashMap::new())));
44
45pub struct SharedClientHandle {
48 client: Arc<Client>,
49 registry: Arc<Mutex<RegistryMap>>,
50 key: ConnectionKey,
51}
52
53impl Debug for SharedClientHandle {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 f.debug_struct(stringify!(SharedClientHandle))
56 .field("key", &self.key)
57 .finish_non_exhaustive()
58 }
59}
60
61impl SharedClientHandle {
62 fn new(client: Arc<Client>, registry: Arc<Mutex<RegistryMap>>, key: ConnectionKey) -> Self {
63 Self {
64 client,
65 registry,
66 key,
67 }
68 }
69
70 pub fn as_arc(&self) -> &Arc<Client> {
72 &self.client
73 }
74}
75
76impl Deref for SharedClientHandle {
77 type Target = Client;
78
79 fn deref(&self) -> &Self::Target {
80 self.client.as_ref()
81 }
82}
83
84impl Drop for SharedClientHandle {
85 fn drop(&mut self) {
86 let mut guard = self.registry.lock();
87 if let Some((_, ref_count)) = guard.get_mut(&self.key) {
88 *ref_count = ref_count.saturating_sub(1);
89 if *ref_count == 0 {
90 guard.remove(&self.key);
91 tracing::debug!(
92 "Shared IB client removed from registry (host={}, port={}, client_id={})",
93 self.key.0,
94 self.key.1,
95 self.key.2
96 );
97 }
98 }
99 }
100}
101
102pub async fn get_or_connect(
110 host: &str,
111 port: u16,
112 client_id: i32,
113 connection_timeout_secs: u64,
114) -> anyhow::Result<SharedClientHandle> {
115 let key = ConnectionKey(host.to_string(), port, client_id);
116 let registry = Arc::clone(®ISTRY);
117
118 log::debug!(
119 "Acquiring shared IB client (host={}, port={}, client_id={}, timeout_secs={})",
120 host,
121 port,
122 client_id,
123 connection_timeout_secs
124 );
125
126 let (reuse_client, ref_count_val) = {
127 let mut guard = registry.lock();
128
129 if let Some((client, ref_count)) = guard.get_mut(&key) {
130 if client.is_connected() {
131 *ref_count += 1;
132 let ref_count_val = *ref_count;
133 let client = Arc::clone(client);
134 (Some(client), ref_count_val)
135 } else {
136 tracing::debug!(
137 "Removing disconnected shared IB client before reconnect (host={}, port={}, client_id={})",
138 host,
139 port,
140 client_id
141 );
142 guard.remove(&key);
143 (None, 0)
144 }
145 } else {
146 (None, 0)
147 }
148 };
149
150 if let Some(client) = reuse_client {
151 log::debug!(
152 "Reusing shared IB client (host={}, port={}, client_id={}, ref_count={})",
153 host,
154 port,
155 client_id,
156 ref_count_val
157 );
158 return Ok(SharedClientHandle::new(client, registry, key));
159 }
160
161 let address = format!("{host}:{port}");
162 let connect_timeout = Duration::from_secs(connection_timeout_secs);
163 log::debug!(
164 "No shared IB client found, establishing new connection to {} with timeout {:?}",
165 address,
166 connect_timeout
167 );
168 let client = tokio::time::timeout(connect_timeout, Client::connect(&address, client_id))
169 .await
170 .map_err(|_| {
171 anyhow::anyhow!(
172 "Timed out connecting to IB Gateway/TWS after {}s",
173 connection_timeout_secs
174 )
175 })?
176 .context("Failed to connect to IB Gateway/TWS")?;
177 let client = Arc::new(client);
178
179 {
180 let mut guard = registry.lock();
181 log::debug!(
182 "Registering shared IB client in registry (host={}, port={}, client_id={})",
183 host,
184 port,
185 client_id
186 );
187 guard.insert(key.clone(), (Arc::clone(&client), 1));
188 }
189
190 tracing::debug!(
191 "Registered new shared IB client (host={}, port={}, client_id={})",
192 host,
193 port,
194 client_id
195 );
196
197 Ok(SharedClientHandle::new(client, registry, key))
198}