ibapi/subscriptions/async.rs
1//! Asynchronous subscription implementation
2
3use std::pin::Pin;
4use std::sync::atomic::{AtomicBool, Ordering};
5use std::sync::Arc;
6use std::task::{Context, Poll};
7use std::time::Duration;
8
9use futures::stream::Stream;
10use futures::StreamExt;
11use log::{debug, warn};
12use tokio::sync::mpsc;
13
14use super::common::{filter_notice, is_undeclared, DecoderContext, RoutedItem, SubscriptionItem};
15use super::StreamDecoder;
16use crate::messages::{IncomingMessages, ResponseMessage};
17use crate::transport::{AsyncInternalSubscription, AsyncMessageBus};
18use crate::Error;
19
20// Type aliases to reduce complexity
21type CancelFn = Box<dyn Fn(i32, Option<i32>, Option<&DecoderContext>) -> Result<Vec<u8>, Error> + Send + Sync>;
22type DecoderFn<T> = Arc<dyn Fn(&DecoderContext, &ResponseMessage) -> Result<T, Error> + Send + Sync>;
23// Non-capturing detector — a plain fn pointer (the decoder's `is_snapshot_end`),
24// so it needs no allocation, no vtable, and is `Copy`.
25type SnapshotEndFn<T> = fn(&T) -> bool;
26
27/// Asynchronous subscription for streaming data.
28///
29/// `Subscription<T>` implements [`futures::Stream`] with
30/// `Item = Result<SubscriptionItem<T>, Error>`:
31///
32/// * `None` — the stream has ended.
33/// * `Some(Ok(SubscriptionItem::Data(t)))` — a decoded value.
34/// * `Some(Ok(SubscriptionItem::Notice(n)))` — a non-fatal IB notice (a warning
35/// code in [`WARNING_CODE_RANGE`](crate::messages::WARNING_CODE_RANGE) or
36/// order-cancel code 202) carried on this subscription's `request_id`; the
37/// stream stays open.
38/// * `Some(Err(e))` — terminal error; subsequent calls return `None`.
39///
40/// Consume via [`StreamExt`](futures::StreamExt):
41///
42/// ```no_run
43/// # use ibapi::Client;
44/// # use ibapi::contracts::Contract;
45/// # use ibapi::subscriptions::SubscriptionItem;
46/// # use futures::StreamExt;
47/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
48/// let client = Client::connect("127.0.0.1:4002", 100).await?;
49/// let contract = Contract::stock("AAPL").build();
50/// let mut subscription = client.market_data(&contract).subscribe().await?;
51///
52/// while let Some(item) = subscription.next().await {
53/// match item {
54/// Ok(SubscriptionItem::Data(tick)) => println!("tick: {tick:?}"),
55/// Ok(SubscriptionItem::Notice(n)) => eprintln!("notice: {n}"),
56/// Err(e) => { eprintln!("error: {e}"); break; }
57/// }
58/// }
59/// # Ok(()) }
60/// ```
61///
62/// When you only care about data, use the [`SubscriptionItemStreamExt::filter_data`]
63/// adapter to filter notices (logged at `warn!`):
64///
65/// ```no_run
66/// # use ibapi::subscriptions::SubscriptionItemStreamExt;
67/// # use futures::StreamExt;
68/// # async fn run(subscription: ibapi::subscriptions::Subscription<i32>) {
69/// let mut data = subscription.filter_data();
70/// while let Some(result) = data.next().await { /* result: Result<i32, _> */ }
71/// # }
72/// ```
73///
74/// Notices that are *not* tied to a specific subscription — connectivity codes
75/// 1100/1101/1102, farm-status 2104/2105/2106/2107/2108, etc. — are not delivered
76/// here. Subscribe to them via [`Client::notice_stream`](crate::Client::notice_stream)
77/// instead.
78#[must_use = "Subscription must be polled (via .next().await or .filter_data()) to receive data; dropping it cancels the request"]
79pub struct Subscription<T> {
80 inner: SubscriptionInner<T>,
81 /// Metadata for cancellation
82 request_id: Option<i32>,
83 order_id: Option<i32>,
84 context: DecoderContext,
85 /// Shared across clones — one `cancel()` call disables future cancel sends from any clone.
86 cancelled: Arc<AtomicBool>,
87 /// Shared across clones — set once a snapshot-end sentinel is observed, so drop/cancel
88 /// skips the redundant cancel for an already-completed snapshot (mirrors the sync side).
89 snapshot_ended: Arc<AtomicBool>,
90 /// Per-clone — each clone has its own `BroadcastStream` position, so a terminal event
91 /// on one clone must not short-circuit other clones' polls.
92 stream_ended: AtomicBool,
93 message_bus: Option<Arc<dyn AsyncMessageBus>>,
94 /// Cancel message generator
95 cancel_fn: Option<Arc<CancelFn>>,
96 /// Snapshot-end detector captured from the decoder (`None` for pre-decoded subscriptions).
97 snapshot_end_fn: Option<SnapshotEndFn<T>>,
98 /// The decoder's declared message types, captured because `poll_next` has
99 /// erased the decoder to a closure. Required, not optional: an absent
100 /// declaration would silently disable the skip filter, which is the failure
101 /// shape `RESPONSE_MESSAGE_IDS` dropped its default to prevent.
102 response_message_ids: &'static [IncomingMessages],
103}
104
105enum SubscriptionInner<T> {
106 /// Subscription with decoder - receives ResponseMessage and decodes to T.
107 /// The `context` for decode lives on the outer `Subscription<T>`.
108 WithDecoder {
109 subscription: AsyncInternalSubscription,
110 decoder: DecoderFn<T>,
111 },
112 /// Pre-decoded subscription - receives T directly
113 PreDecoded { receiver: mpsc::UnboundedReceiver<Result<T, Error>> },
114}
115
116impl<T> Clone for SubscriptionInner<T> {
117 fn clone(&self) -> Self {
118 match self {
119 SubscriptionInner::WithDecoder { subscription, decoder } => SubscriptionInner::WithDecoder {
120 subscription: subscription.clone(),
121 decoder: decoder.clone(),
122 },
123 SubscriptionInner::PreDecoded { .. } => {
124 // Can't clone mpsc receivers
125 panic!("Cannot clone pre-decoded subscriptions");
126 }
127 }
128 }
129}
130
131impl<T> Clone for Subscription<T> {
132 fn clone(&self) -> Self {
133 Self {
134 inner: self.inner.clone(),
135 request_id: self.request_id,
136 order_id: self.order_id,
137 context: self.context.clone(),
138 cancelled: self.cancelled.clone(),
139 snapshot_ended: self.snapshot_ended.clone(),
140 // Clone gets a fresh stream_ended — independent BroadcastStream position.
141 stream_ended: AtomicBool::new(false),
142 message_bus: self.message_bus.clone(),
143 cancel_fn: self.cancel_fn.clone(),
144 snapshot_end_fn: self.snapshot_end_fn,
145 response_message_ids: self.response_message_ids,
146 }
147 }
148}
149
150impl<T> Subscription<T> {
151 /// Create a subscription from an internal subscription and a decoder.
152 ///
153 /// `pub(crate)` because the parameter types (`AsyncInternalSubscription`,
154 /// `DecoderContext`) are not part of the public API. External callers
155 /// reach subscriptions via the typed builders on `Client`.
156 pub(crate) fn with_decoder<D>(
157 internal: AsyncInternalSubscription,
158 message_bus: Arc<dyn AsyncMessageBus>,
159 decoder: D,
160 response_message_ids: &'static [IncomingMessages],
161 request_id: Option<i32>,
162 order_id: Option<i32>,
163 context: DecoderContext,
164 ) -> Self
165 where
166 D: Fn(&DecoderContext, &ResponseMessage) -> Result<T, Error> + Send + Sync + 'static,
167 {
168 Self {
169 inner: SubscriptionInner::WithDecoder {
170 subscription: internal,
171 decoder: Arc::new(decoder),
172 },
173 request_id,
174 order_id,
175 context,
176 cancelled: Arc::new(AtomicBool::new(false)),
177 snapshot_ended: Arc::new(AtomicBool::new(false)),
178 stream_ended: AtomicBool::new(false),
179 message_bus: Some(message_bus),
180 cancel_fn: None,
181 snapshot_end_fn: None,
182 response_message_ids,
183 }
184 }
185
186 /// Create a subscription from an internal subscription using the DataStream decoder
187 pub(crate) fn new_from_internal<D>(
188 internal: AsyncInternalSubscription,
189 message_bus: Arc<dyn AsyncMessageBus>,
190 request_id: Option<i32>,
191 order_id: Option<i32>,
192 context: DecoderContext,
193 ) -> Self
194 where
195 D: StreamDecoder<T> + 'static,
196 T: StreamDecoder<T> + 'static,
197 {
198 super::common::debug_assert_request_id_routable::<T, D>(request_id);
199
200 let mut sub = Self::with_decoder(internal, message_bus, D::decode, D::RESPONSE_MESSAGE_IDS, request_id, order_id, context);
201 sub.cancel_fn = Some(Arc::new(Box::new(D::cancel_message)));
202 // Capture the decoder's snapshot-end detector so `poll_next` (which lacks the
203 // `StreamDecoder` bound) can flag a completed snapshot and skip the cancel on
204 // drop — the async mirror of the sync side's intrinsic `snapshot_ended` tracking.
205 sub.snapshot_end_fn = Some(<T as StreamDecoder<T>>::is_snapshot_end);
206 sub
207 }
208
209 /// Create a subscription from internal subscription without explicit metadata.
210 /// AsyncInternalSubscription's Drop carries the cancel signal, so no cancel-fn metadata.
211 pub(crate) fn new_from_internal_simple<D>(
212 internal: AsyncInternalSubscription,
213 message_bus: Arc<dyn AsyncMessageBus>,
214 context: DecoderContext,
215 ) -> Self
216 where
217 D: StreamDecoder<T> + 'static,
218 T: StreamDecoder<T> + 'static,
219 {
220 Self::new_from_internal::<D>(internal, message_bus, None, None, context)
221 }
222
223 /// Create subscription from existing receiver (for backward compatibility)
224 pub fn new(receiver: mpsc::UnboundedReceiver<Result<T, Error>>) -> Self {
225 // This creates a subscription that expects pre-decoded messages
226 // Used for compatibility with existing code that manually decodes
227 Self {
228 inner: SubscriptionInner::PreDecoded { receiver },
229 request_id: None,
230 order_id: None,
231 context: DecoderContext::default(),
232 cancelled: Arc::new(AtomicBool::new(false)),
233 snapshot_ended: Arc::new(AtomicBool::new(false)),
234 stream_ended: AtomicBool::new(false),
235 message_bus: None,
236 cancel_fn: None,
237 snapshot_end_fn: None,
238 // Pre-decoded subscriptions never reach the filter (other poll_next arm).
239 response_message_ids: &[],
240 }
241 }
242
243 /// Get the request ID associated with this subscription
244 pub fn request_id(&self) -> Option<i32> {
245 self.request_id
246 }
247}
248
249#[allow(private_bounds)]
250impl<T: StreamDecoder<T> + Send + 'static> Subscription<T> {
251 /// Collects data items into a `Vec`, bounded by a total wall-clock `timeout`.
252 ///
253 /// Drives the subscription until the first of: the `timeout` elapses, the
254 /// stream ends, a snapshot-end sentinel arrives (e.g.
255 /// [`TickTypes::SnapshotEnd`](crate::market_data::realtime::TickTypes::SnapshotEnd)),
256 /// or a terminal error occurs. Notices are filtered (logged at `warn!`); the
257 /// snapshot-end sentinel is not included in the returned `Vec`. On a terminal
258 /// error the items collected so far are returned (the error is logged at
259 /// `warn!`).
260 ///
261 /// This is the one-shot snapshot terminal: combined with
262 /// [`MarketDataBuilder::snapshot`](crate::market_data::realtime::MarketDataBuilder::snapshot),
263 /// the request returns one round of data ending in a snapshot sentinel, so
264 /// `timeout` acts only as a safety bound. Equivalent to
265 /// [`collect_until`](Self::collect_until) with a predicate that never fires.
266 ///
267 /// # Examples
268 ///
269 /// ```no_run
270 /// use ibapi::prelude::*;
271 /// use std::time::Duration;
272 ///
273 /// #[tokio::main]
274 /// async fn main() {
275 /// let client = Client::connect("127.0.0.1:4002", 100).await.expect("connection failed");
276 /// let contract = Contract::stock("AAPL").build();
277 /// let mut subscription = client.market_data(&contract).snapshot().subscribe().await.expect("request failed");
278 ///
279 /// let ticks = subscription.collect_for(Duration::from_secs(5)).await;
280 /// println!("collected {} ticks", ticks.len());
281 /// }
282 /// ```
283 pub async fn collect_for(&mut self, timeout: Duration) -> Vec<T> {
284 self.collect_until(timeout, |_| false).await
285 }
286
287 /// Collects data items into a `Vec`, stopping early once `stop` is satisfied.
288 ///
289 /// Like [`collect_for`](Self::collect_for), but after each item is appended
290 /// the `stop` predicate is called with the full accumulated slice; returning
291 /// `true` ends collection (the triggering item is included). Use it to stop
292 /// as soon as the fields of interest are populated, rather than waiting out
293 /// the whole `timeout`. The same timeout / stream-end / snapshot-end /
294 /// terminal-error bounds as `collect_for` still apply.
295 ///
296 /// # Examples
297 ///
298 /// ```no_run
299 /// use ibapi::market_data::realtime::TickTypes;
300 /// use ibapi::prelude::*;
301 /// use std::time::Duration;
302 ///
303 /// #[tokio::main]
304 /// async fn main() {
305 /// let client = Client::connect("127.0.0.1:4002", 100).await.expect("connection failed");
306 /// let contract = Contract::stock("AAPL").build();
307 /// let mut subscription = client.market_data(&contract).snapshot().subscribe().await.expect("request failed");
308 ///
309 /// // Stop as soon as a price tick has arrived.
310 /// let ticks = subscription
311 /// .collect_until(Duration::from_secs(5), |ticks| {
312 /// ticks.iter().any(|t| matches!(t, TickTypes::Price(_) | TickTypes::PriceSize(_)))
313 /// })
314 /// .await;
315 /// println!("collected {} ticks", ticks.len());
316 /// }
317 /// ```
318 pub async fn collect_until(&mut self, timeout: Duration, mut stop: impl FnMut(&[T]) -> bool) -> Vec<T> {
319 let deadline = tokio::time::Instant::now() + timeout;
320 let mut collected = Vec::new();
321 loop {
322 match tokio::time::timeout_at(deadline, self.next()).await {
323 // Total deadline reached.
324 Err(_elapsed) => break,
325 // End of stream.
326 Ok(None) => break,
327 Ok(Some(Ok(SubscriptionItem::Data(value)))) => {
328 if value.is_snapshot_end() {
329 break;
330 }
331 collected.push(value);
332 if stop(&collected) {
333 break;
334 }
335 }
336 Ok(Some(Ok(SubscriptionItem::Notice(notice)))) => warn!("ib notice on subscription: {notice}"),
337 Ok(Some(Err(e))) => {
338 warn!("subscription error during collect: {e}");
339 break;
340 }
341 }
342 }
343 collected
344 }
345}
346
347impl<T: Send + 'static> Stream for Subscription<T> {
348 type Item = Result<SubscriptionItem<T>, Error>;
349
350 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
351 // Subscription<T> is auto-Unpin: BroadcastStream uses ReusableBoxFuture
352 // (boxed → Unpin externally), mpsc::UnboundedReceiver is Unpin, and
353 // every other field is Unpin. Safe to project to &mut Self.
354 let this = self.get_mut();
355
356 if this.stream_ended.load(Ordering::Relaxed) {
357 return Poll::Ready(None);
358 }
359
360 let Subscription {
361 inner,
362 context,
363 stream_ended,
364 snapshot_ended,
365 snapshot_end_fn,
366 response_message_ids,
367 ..
368 } = this;
369 loop {
370 match inner {
371 SubscriptionInner::WithDecoder { subscription, decoder } => {
372 // Drain the BroadcastStream synchronously while items are
373 // ready, so skipped frames don't re-yield to the executor
374 // between immediately-available items.
375 // Lag is converted to an in-band gap notice inside
376 // `poll_next_routed` (#779); the Notice arm below delivers it.
377 let routed = match subscription.poll_next_routed(cx) {
378 Poll::Ready(Some(item)) => item,
379 Poll::Ready(None) => return Poll::Ready(None),
380 Poll::Pending => return Poll::Pending,
381 };
382
383 match routed {
384 RoutedItem::Response(message) => {
385 if is_undeclared(response_message_ids, &message) {
386 log::trace!("skipping {:?} — not declared by this subscription's decoder", message.message_type());
387 continue;
388 }
389 match decoder(context, &message) {
390 Ok(val) => {
391 if snapshot_end_fn.is_some_and(|is_end| is_end(&val)) {
392 snapshot_ended.store(true, Ordering::Relaxed);
393 }
394 return Poll::Ready(Some(Ok(SubscriptionItem::Data(val))));
395 }
396 Err(Error::EndOfStream) => {
397 stream_ended.store(true, Ordering::Relaxed);
398 return Poll::Ready(None);
399 }
400 Err(err) => {
401 stream_ended.store(true, Ordering::Relaxed);
402 return Poll::Ready(Some(Err(err)));
403 }
404 }
405 }
406 RoutedItem::Notice(notice) => return Poll::Ready(Some(Ok(SubscriptionItem::Notice(notice)))),
407 RoutedItem::Error(Error::EndOfStream) => {
408 stream_ended.store(true, Ordering::Relaxed);
409 return Poll::Ready(None);
410 }
411 RoutedItem::Error(e) => {
412 stream_ended.store(true, Ordering::Relaxed);
413 return Poll::Ready(Some(Err(e)));
414 }
415 }
416 }
417 SubscriptionInner::PreDecoded { receiver } => {
418 return match receiver.poll_recv(cx) {
419 Poll::Ready(Some(Ok(t))) => Poll::Ready(Some(Ok(SubscriptionItem::Data(t)))),
420 Poll::Ready(Some(Err(e))) => {
421 stream_ended.store(true, Ordering::Relaxed);
422 Poll::Ready(Some(Err(e)))
423 }
424 Poll::Ready(None) => Poll::Ready(None),
425 Poll::Pending => Poll::Pending,
426 };
427 }
428 }
429 }
430 }
431}
432
433impl<T> Subscription<T> {
434 /// Cancel the subscription
435 pub async fn cancel(&self) {
436 // Snapshot subscriptions self-terminate after the snapshot-end sentinel;
437 // their request is already complete, so skip the redundant cancel.
438 if self.snapshot_ended.load(Ordering::Relaxed) {
439 return;
440 }
441
442 if self.cancelled.load(Ordering::Relaxed) {
443 return;
444 }
445
446 self.cancelled.store(true, Ordering::Relaxed);
447
448 if let (Some(message_bus), Some(cancel_fn)) = (&self.message_bus, &self.cancel_fn) {
449 let id = self.request_id.or(self.order_id);
450 if let Ok(message) = cancel_fn(self.context.server_version, id, Some(&self.context)) {
451 if let Err(e) = message_bus.send_message(message).await {
452 warn!("error sending cancel message: {e}")
453 }
454 }
455 }
456 }
457}
458
459impl<T> Drop for Subscription<T> {
460 fn drop(&mut self) {
461 debug!("dropping async subscription");
462
463 // A completed snapshot needs no cancel — mirror the sync drop behavior.
464 if self.snapshot_ended.load(Ordering::Relaxed) {
465 return;
466 }
467
468 // Check if already cancelled
469 if self.cancelled.load(Ordering::Relaxed) {
470 return;
471 }
472
473 self.cancelled.store(true, Ordering::Relaxed);
474
475 // Try to send cancel message if we have the necessary components
476 if let (Some(message_bus), Some(cancel_fn)) = (&self.message_bus, &self.cancel_fn) {
477 let message_bus = message_bus.clone();
478 let id = self.request_id.or(self.order_id);
479 let context = self.context.clone();
480
481 if let Ok(message) = cancel_fn(context.server_version, id, Some(&context)) {
482 // Drop can't be async; spawn the cancel send so it actually goes out.
483 tokio::spawn(async move {
484 if let Err(e) = message_bus.send_message(message).await {
485 warn!("error sending cancel message in drop: {e}");
486 }
487 });
488 }
489 }
490 }
491}
492
493/// Stream adapter that filters `SubscriptionItem::Notice` items (logging them
494/// at `warn!`) from any `Stream<Item = Result<SubscriptionItem<T>, Error>>` and
495/// yields the underlying `Result<T, Error>` to the caller.
496///
497/// Returned by [`SubscriptionItemStreamExt::filter_data`]. Async mirror of the
498/// sync `FilterData` iterator adapter.
499#[must_use = "streams are lazy and do nothing unless polled"]
500pub struct FilterDataStream<S> {
501 inner: S,
502}
503
504impl<S, T> Stream for FilterDataStream<S>
505where
506 S: Stream<Item = Result<SubscriptionItem<T>, Error>> + Unpin,
507{
508 type Item = Result<T, Error>;
509
510 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
511 loop {
512 match Pin::new(&mut self.inner).poll_next(cx) {
513 Poll::Ready(Some(item)) => {
514 if let Some(out) = filter_notice(item) {
515 return Poll::Ready(Some(out));
516 }
517 // Filtered Notice; loop and poll again.
518 }
519 Poll::Ready(None) => return Poll::Ready(None),
520 Poll::Pending => return Poll::Pending,
521 }
522 }
523 }
524}
525
526/// Extension trait that adds [`filter_data`](SubscriptionItemStreamExt::filter_data)
527/// to any stream yielding `Result<SubscriptionItem<T>, Error>`. Async mirror of
528/// the sync `SubscriptionItemIterExt`.
529///
530/// Use it for the data-only flow when consuming a [`Subscription`]:
531///
532/// ```no_run
533/// # use ibapi::subscriptions::{Subscription, SubscriptionItemStreamExt};
534/// # use futures::StreamExt;
535/// # async fn run(subscription: Subscription<i32>) {
536/// let mut data = subscription.filter_data();
537/// while let Some(result) = data.next().await { /* result: Result<i32, _> */ }
538/// # }
539/// ```
540pub trait SubscriptionItemStreamExt: Stream + Sized {
541 /// Wrap `self` in a [`FilterDataStream`] adapter that drops
542 /// `SubscriptionItem::Notice` items (logging them) and yields the
543 /// underlying `Result<T, Error>`.
544 fn filter_data<T>(self) -> FilterDataStream<Self>
545 where
546 Self: Stream<Item = Result<SubscriptionItem<T>, Error>>,
547 {
548 FilterDataStream { inner: self }
549 }
550}
551
552impl<S: Stream + Sized> SubscriptionItemStreamExt for S {}
553
554#[cfg(all(test, feature = "async"))]
555#[path = "async_tests.rs"]
556mod tests;