Skip to main content

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;