1use std::{
23 collections::HashMap,
24 fmt::Debug,
25 future::Future,
26 ops::Deref,
27 sync::{Arc, LazyLock},
28 time::Duration,
29};
30
31use anyhow::Context;
32use ibapi::{Error, client::Client};
33use parking_lot::Mutex;
34
35#[derive(Clone, Debug, Eq, Hash, PartialEq)]
37struct ConnectionKey(String, u16, i32);
38
39enum RegistryEntry<T> {
40 Connected {
41 client: Arc<T>,
42 ref_count: u32,
43 },
44 Connecting {
45 flight: Arc<InFlight<T>>,
46 ref_count: u32,
47 },
48}
49
50struct InFlight<T> {
51 result: Mutex<Option<Result<Arc<T>, String>>>,
52 notify: tokio::sync::Notify,
53}
54
55impl<T> Default for InFlight<T> {
56 fn default() -> Self {
57 Self {
58 result: Mutex::new(None),
59 notify: tokio::sync::Notify::new(),
60 }
61 }
62}
63
64impl<T> InFlight<T> {
65 fn complete(&self, result: Result<Arc<T>, String>) {
66 *self.result.lock() = Some(result);
67 self.notify.notify_waiters();
68 }
69
70 async fn wait(&self) -> anyhow::Result<Arc<T>> {
71 loop {
72 let notified = self.notify.notified();
73
74 if let Some(result) = self.result.lock().clone() {
75 return result.map_err(anyhow::Error::msg);
76 }
77
78 notified.await;
79 }
80 }
81}
82
83struct SharedRegistry<T> {
84 entries: Mutex<HashMap<ConnectionKey, RegistryEntry<T>>>,
85}
86
87impl<T> Default for SharedRegistry<T> {
88 fn default() -> Self {
89 Self {
90 entries: Mutex::new(HashMap::new()),
91 }
92 }
93}
94
95impl<T> SharedRegistry<T> {
96 async fn acquire<Connect, ConnectFuture, IsConnected>(
97 &self,
98 key: ConnectionKey,
99 is_connected: IsConnected,
100 connect: Connect,
101 ) -> anyhow::Result<Arc<T>>
102 where
103 Connect: FnOnce() -> ConnectFuture,
104 ConnectFuture: Future<Output = anyhow::Result<T>>,
105 IsConnected: Fn(&T) -> bool,
106 {
107 let (flight, should_connect) = {
108 let mut entries = self.entries.lock();
109
110 match entries.get_mut(&key) {
111 Some(RegistryEntry::Connected { client, ref_count }) if is_connected(client) => {
112 *ref_count = ref_count
113 .checked_add(1)
114 .context("Shared IB client reference count overflow")?;
115 return Ok(Arc::clone(client));
116 }
117 Some(RegistryEntry::Connecting { flight, .. }) => (Arc::clone(flight), false),
118 Some(RegistryEntry::Connected { .. }) | None => {
119 let flight = Arc::new(InFlight::default());
120 entries.insert(
121 key.clone(),
122 RegistryEntry::Connecting {
123 flight: Arc::clone(&flight),
124 ref_count: 1,
125 },
126 );
127
128 (flight, true)
129 }
130 }
131 };
132
133 if !should_connect {
134 let client = flight.wait().await?;
135 let mut entries = self.entries.lock();
136 match entries.get_mut(&key) {
137 Some(RegistryEntry::Connected {
138 client: current,
139 ref_count,
140 }) if Arc::ptr_eq(current, &client) => {
141 *ref_count = ref_count
142 .checked_add(1)
143 .context("Shared IB client reference count overflow")?;
144 }
145 None => {
146 entries.insert(
147 key,
148 RegistryEntry::Connected {
149 client: Arc::clone(&client),
150 ref_count: 1,
151 },
152 );
153 }
154 _ => anyhow::bail!("Shared IB client was replaced while acquisition was waiting"),
155 }
156
157 return Ok(client);
158 }
159
160 let mut guard = ConnectGuard {
161 registry: self,
162 key: key.clone(),
163 flight: Arc::clone(&flight),
164 armed: true,
165 };
166
167 let result = connect().await.map(Arc::new).map_err(|e| format!("{e:#}"));
168 {
169 let mut entries = self.entries.lock();
170
171 let ref_count = match entries.get(&key) {
172 Some(RegistryEntry::Connecting {
173 flight: current,
174 ref_count,
175 }) if Arc::ptr_eq(current, &flight) => *ref_count,
176 _ => anyhow::bail!("Shared IB client in-flight registry entry was replaced"),
177 };
178
179 match &result {
180 Ok(client) => {
181 entries.insert(
182 key.clone(),
183 RegistryEntry::Connected {
184 client: Arc::clone(client),
185 ref_count,
186 },
187 );
188 }
189 Err(_) => {
190 entries.remove(&key);
191 }
192 }
193 }
194
195 flight.complete(result.clone());
196 guard.armed = false;
197 result.map_err(anyhow::Error::msg)
198 }
199
200 fn release(&self, key: &ConnectionKey, client: &Arc<T>) {
201 let mut entries = self.entries.lock();
202
203 let Some(RegistryEntry::Connected {
204 client: registered,
205 ref_count,
206 }) = entries.get_mut(key)
207 else {
208 return;
209 };
210
211 if !Arc::ptr_eq(registered, client) {
212 return;
213 }
214
215 *ref_count = ref_count.saturating_sub(1);
216 if *ref_count == 0 {
217 entries.remove(key);
218 }
219 }
220
221 fn ref_count(&self, key: &ConnectionKey) -> Option<u32> {
222 match self.entries.lock().get(key) {
223 Some(
224 RegistryEntry::Connected { ref_count, .. }
225 | RegistryEntry::Connecting { ref_count, .. },
226 ) => Some(*ref_count),
227 None => None,
228 }
229 }
230}
231
232struct ConnectGuard<'a, T> {
235 registry: &'a SharedRegistry<T>,
236 key: ConnectionKey,
237 flight: Arc<InFlight<T>>,
238 armed: bool,
239}
240
241impl<T> Drop for ConnectGuard<'_, T> {
242 fn drop(&mut self) {
243 if !self.armed {
244 return;
245 }
246
247 let mut entries = self.registry.entries.lock();
248 if let Some(RegistryEntry::Connecting { flight, .. }) = entries.get(&self.key)
249 && Arc::ptr_eq(flight, &self.flight)
250 {
251 entries.remove(&self.key);
252 }
253
254 drop(entries);
255 self.flight
256 .complete(Err(String::from("Shared IB client connect was cancelled")));
257 }
258}
259
260static REGISTRY: LazyLock<Arc<SharedRegistry<Client>>> =
262 LazyLock::new(|| Arc::new(SharedRegistry::default()));
263
264pub struct SharedClientHandle {
267 client: Arc<Client>,
268 registry: Arc<SharedRegistry<Client>>,
269 key: ConnectionKey,
270}
271
272impl Debug for SharedClientHandle {
273 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
274 f.debug_struct(stringify!(SharedClientHandle))
275 .field("key", &self.key)
276 .finish_non_exhaustive()
277 }
278}
279
280impl SharedClientHandle {
281 fn new(client: Arc<Client>, registry: Arc<SharedRegistry<Client>>, key: ConnectionKey) -> Self {
282 Self {
283 client,
284 registry,
285 key,
286 }
287 }
288
289 pub fn as_arc(&self) -> &Arc<Client> {
291 &self.client
292 }
293}
294
295impl Deref for SharedClientHandle {
296 type Target = Client;
297
298 fn deref(&self) -> &Self::Target {
299 self.client.as_ref()
300 }
301}
302
303impl Drop for SharedClientHandle {
304 fn drop(&mut self) {
305 self.registry.release(&self.key, &self.client);
306 }
307}
308
309pub async fn get_or_connect(
317 host: &str,
318 port: u16,
319 client_id: i32,
320 connection_timeout_secs: u64,
321) -> anyhow::Result<SharedClientHandle> {
322 let key = ConnectionKey(host.to_string(), port, client_id);
323 let registry = Arc::clone(®ISTRY);
324
325 log::debug!(
326 "Acquiring shared IB client (host={host}, port={port}, client_id={client_id}, timeout_secs={connection_timeout_secs})"
327 );
328
329 let address = format!("{host}:{port}");
330 let connect_timeout = Duration::from_secs(connection_timeout_secs);
331
332 let client = registry
333 .acquire(key.clone(), Client::is_connected, || async move {
334 log::debug!(
335 "Establishing shared IB connection to {address} with timeout {connect_timeout:?}"
336 );
337 tokio::time::timeout(connect_timeout, Client::connect(&address, client_id))
338 .await
339 .map_err(|_| {
340 anyhow::anyhow!(
341 "Timed out connecting to IB Gateway/TWS after {connection_timeout_secs}s; verify socket API settings and use a TWS/IB Gateway version supported by the IB integration guide"
342 )
343 })?
344 .map_err(connection_error)
345 .context("Failed to connect to IB Gateway/TWS")
346 })
347 .await?;
348
349 if registry.ref_count(&key).is_some_and(|count| count > 1) {
350 log::debug!("Reusing shared IB client (host={host}, port={port}, client_id={client_id})");
351 }
352
353 Ok(SharedClientHandle::new(client, registry, key))
354}
355
356fn connection_error(e: Error) -> anyhow::Error {
357 match e {
358 Error::ServerVersion(required, actual, _) => anyhow::anyhow!(
359 "TWS/IB Gateway server protocol {actual} is unsupported; server protocol {required} or newer is required. Upgrade TWS/IB Gateway"
360 ),
361 e => anyhow::Error::new(e),
362 }
363}
364
365#[cfg(test)]
366mod tests {
367 use std::{
368 sync::atomic::{AtomicUsize, Ordering},
369 task::{Context as TaskContext, Waker},
370 };
371
372 use rstest::rstest;
373 use tokio::io::{AsyncReadExt, AsyncWriteExt};
374
375 use super::*;
376
377 #[derive(Debug)]
378 struct TestClient;
379
380 #[rstest]
381 #[tokio::test]
382 async fn concurrent_acquire_uses_one_connection_and_reserves_both_references() {
383 let registry = Arc::new(SharedRegistry::default());
384 let key = ConnectionKey(String::from("127.0.0.1"), 7_497, 42);
385 let connect_count = Arc::new(AtomicUsize::new(0));
386 let connect_started = Arc::new(tokio::sync::Notify::new());
387 let release_connect = Arc::new(tokio::sync::Notify::new());
388
389 let first = {
390 let registry = Arc::clone(®istry);
391 let key = key.clone();
392 let connect_count = Arc::clone(&connect_count);
393 let connect_started = Arc::clone(&connect_started);
394 let release_connect = Arc::clone(&release_connect);
395
396 tokio::spawn(async move {
397 registry
398 .acquire(
399 key,
400 |_| true,
401 || async move {
402 connect_count.fetch_add(1, Ordering::SeqCst);
403 connect_started.notify_one();
404 release_connect.notified().await;
405 Ok::<_, anyhow::Error>(TestClient)
406 },
407 )
408 .await
409 })
410 };
411
412 connect_started.notified().await;
413
414 let second = registry.acquire(
415 key.clone(),
416 |_| true,
417 || async {
418 connect_count.fetch_add(1, Ordering::SeqCst);
419 anyhow::bail!("second connector must not run")
420 },
421 );
422
423 tokio::pin!(second);
424 assert!(
425 second
426 .as_mut()
427 .poll(&mut TaskContext::from_waker(Waker::noop()))
428 .is_pending()
429 );
430 assert_eq!(registry.ref_count(&key), Some(1));
431
432 release_connect.notify_one();
433
434 let first_client = first.await.unwrap().unwrap();
435 let second_client = second.await.unwrap();
436
437 assert!(Arc::ptr_eq(&first_client, &second_client));
438 assert_eq!(connect_count.load(Ordering::SeqCst), 1);
439 assert_eq!(registry.ref_count(&key), Some(2));
440
441 registry.release(&key, &first_client);
442 assert_eq!(registry.ref_count(&key), Some(1));
443 registry.release(&key, &second_client);
444 assert_eq!(registry.ref_count(&key), None);
445 }
446
447 #[rstest]
448 #[tokio::test]
449 async fn cancelled_connect_releases_key_and_fails_waiters() {
450 let registry = Arc::new(SharedRegistry::default());
451 let key = ConnectionKey(String::from("127.0.0.1"), 7_497, 43);
452 let connect_started = Arc::new(tokio::sync::Notify::new());
453
454 let winner = {
455 let registry = Arc::clone(®istry);
456 let key = key.clone();
457 let connect_started = Arc::clone(&connect_started);
458
459 tokio::spawn(async move {
460 registry
461 .acquire(
462 key,
463 |_| true,
464 || async move {
465 connect_started.notify_one();
466 std::future::pending::<()>().await;
467 Ok::<_, anyhow::Error>(TestClient)
468 },
469 )
470 .await
471 })
472 };
473
474 connect_started.notified().await;
475
476 let waiter = registry.acquire(
477 key.clone(),
478 |_| true,
479 || async { anyhow::bail!("waiter must not connect while flight is active") },
480 );
481 tokio::pin!(waiter);
482 assert!(
483 waiter
484 .as_mut()
485 .poll(&mut TaskContext::from_waker(Waker::noop()))
486 .is_pending()
487 );
488
489 winner.abort();
490 assert!(winner.await.unwrap_err().is_cancelled());
491
492 let waiter_error = tokio::time::timeout(Duration::from_secs(1), waiter)
493 .await
494 .expect("waiter must fail fast after cancellation")
495 .unwrap_err();
496 assert_eq!(
497 waiter_error.to_string(),
498 "Shared IB client connect was cancelled"
499 );
500 assert_eq!(registry.ref_count(&key), None);
501
502 let client = registry
503 .acquire(
504 key.clone(),
505 |_| true,
506 || async move { Ok::<_, anyhow::Error>(TestClient) },
507 )
508 .await
509 .unwrap();
510 assert_eq!(registry.ref_count(&key), Some(1));
511 registry.release(&key, &client);
512 assert_eq!(registry.ref_count(&key), None);
513 }
514 #[rstest]
515 #[case(1, false)]
516 #[case(3, false)]
517 #[case(1, true)]
518 #[tokio::test]
519 async fn cancelled_waiters_do_not_retain_connection(
520 #[case] waiter_count: usize,
521 #[case] cancel_after_connect: bool,
522 ) {
523 let registry = SharedRegistry::default();
524 let key = ConnectionKey(String::from("127.0.0.1"), 7_497, 44);
525 let release_connect = tokio::sync::Notify::new();
526
527 let winner = registry.acquire(
528 key.clone(),
529 |_| true,
530 || async {
531 release_connect.notified().await;
532 Ok(TestClient)
533 },
534 );
535
536 tokio::pin!(winner);
537 assert!(
538 winner
539 .as_mut()
540 .poll(&mut TaskContext::from_waker(Waker::noop()))
541 .is_pending()
542 );
543
544 let mut waiters = (0..waiter_count)
545 .map(|_| {
546 Box::pin(registry.acquire(
547 key.clone(),
548 |_| true,
549 || async { anyhow::bail!("waiter must not connect") },
550 ))
551 })
552 .collect::<Vec<_>>();
553
554 for waiter in &mut waiters {
555 assert!(
556 waiter
557 .as_mut()
558 .poll(&mut TaskContext::from_waker(Waker::noop()))
559 .is_pending()
560 );
561 }
562
563 if !cancel_after_connect {
564 waiters.clear();
565 }
566
567 release_connect.notify_one();
568 let client = winner.await.unwrap();
569 waiters.clear();
570 assert_eq!(registry.ref_count(&key), Some(1));
571 registry.release(&key, &client);
572 assert_eq!(registry.ref_count(&key), None);
573 }
574
575 #[rstest]
576 #[tokio::test]
577 async fn cancelled_old_waiter_preserves_replacement_connection() {
578 let registry = SharedRegistry::default();
579 let key = ConnectionKey(String::from("127.0.0.1"), 7_497, 45);
580 let mut winner = Box::pin(registry.acquire(
581 key.clone(),
582 |_| true,
583 || async { std::future::pending::<anyhow::Result<TestClient>>().await },
584 ));
585 assert!(
586 winner
587 .as_mut()
588 .poll(&mut TaskContext::from_waker(Waker::noop()))
589 .is_pending()
590 );
591 let mut waiter = Box::pin(registry.acquire(
592 key.clone(),
593 |_| true,
594 || async { anyhow::bail!("waiter must not connect") },
595 ));
596 assert!(
597 waiter
598 .as_mut()
599 .poll(&mut TaskContext::from_waker(Waker::noop()))
600 .is_pending()
601 );
602 drop(winner);
603 let replacement = registry
604 .acquire(key.clone(), |_| true, || async { Ok(TestClient) })
605 .await
606 .unwrap();
607 drop(waiter);
608 assert_eq!(registry.ref_count(&key), Some(1));
609 registry.release(&key, &replacement);
610 assert_eq!(registry.ref_count(&key), None);
611 }
612 #[rstest]
613 #[case(187)]
614 #[case(212)]
615 #[tokio::test]
616 async fn unsupported_server_is_rejected_before_start_api(#[case] server_version: i32) {
617 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
618 let port = listener.local_addr().unwrap().port();
619
620 let server = tokio::spawn(async move {
621 for _ in 0..2 {
622 let (mut socket, _) = listener.accept().await.unwrap();
623 let mut magic = [0_u8; 4];
624 socket.read_exact(&mut magic).await.unwrap();
625 let size = socket.read_u32().await.unwrap();
626 let mut version = vec![0_u8; size as usize];
627 socket.read_exact(&mut version).await.unwrap();
628 let version = String::from_utf8(version).unwrap();
629 let response = format!("{server_version}\020260911 12:00:00 UTC\0");
630 socket.write_u32(response.len() as u32).await.unwrap();
631 socket.write_all(response.as_bytes()).await.unwrap();
632 let mut extra = [0_u8; 1];
633 let read = tokio::time::timeout(Duration::from_secs(2), socket.read(&mut extra))
634 .await
635 .unwrap()
636 .unwrap();
637 assert_eq!(magic, *b"API\0");
638 assert_eq!(version.split("..").next(), Some("v213"));
639 assert_eq!(
640 read, 0,
641 "unsupported servers must receive no StartApi message"
642 );
643 }
644 });
645
646 let mut errors = Vec::new();
647 for _ in 0..2 {
648 errors.push(
649 get_or_connect("127.0.0.1", port, 917, 1)
650 .await
651 .unwrap_err()
652 .to_string(),
653 );
654 }
655
656 server.await.unwrap();
657 let expected = format!(
658 "Failed to connect to IB Gateway/TWS: TWS/IB Gateway server protocol {server_version} is unsupported; server protocol 213 or newer is required. Upgrade TWS/IB Gateway"
659 );
660 assert_eq!(errors, vec![expected.clone(), expected]);
661 assert_eq!(
662 REGISTRY.ref_count(&ConnectionKey("127.0.0.1".to_string(), port, 917)),
663 None
664 );
665 }
666}