Skip to main content

brokapi_finam/
pump.rs

1//! Event pump: frames -> core -> per-subscription buffers -> the client's `Sender`.
2//!
3//! Never blocks on a slow consumer: events wait in the subscription's buffer, and the
4//! pump hands them to `out` only when it has room (`reserve`). A `Lossless` buffer that fills up
5//! is terminal: one `Event::Overflow`, `on_overflow` (connector goes `Degraded`), later events of
6//! that subscription are dropped until `reconcile` clears the state.
7
8use std::collections::{HashSet, VecDeque};
9use std::sync::{Arc, Mutex};
10use std::time::Instant;
11
12use brokapi_core::{Envelope, Event, SubscriptionId, Timestamps};
13use brokapi_runtime::{BackpressureManager, PushResult};
14use brokapi_sansio::CoreEvent;
15use tokio::sync::mpsc;
16use tokio_util::sync::CancellationToken;
17use tracing::{debug, warn};
18
19use crate::util::now_nanos;
20
21/// The per-subscription buffers, shared between the pump and the connector (which registers and
22/// unregisters subscriptions in it).
23pub type SharedBackpressure = Arc<Mutex<BackpressureManager>>;
24
25/// The channels and handles one [`run_pump`] task works with.
26///
27/// `F` is the frame type of the connector (`MdFrame` or `UdFrame`).
28pub struct PumpIo<F> {
29    /// Frames from the stream tasks.
30    pub rx: mpsc::Receiver<F>,
31    /// Where the envelopes go: the channel the user gave to `start`.
32    pub out: mpsc::Sender<Envelope>,
33    /// Source label written into every envelope.
34    pub source: Arc<str>,
35    /// The per-subscription buffers that hold events until `out` has room.
36    pub backpressure: SharedBackpressure,
37    /// Stops the pump. A child of the connector's root token.
38    pub cancel: CancellationToken,
39}
40
41/// Events that must reach the consumer in order and must never be conflated away.
42fn is_control(ev: &Event) -> bool {
43    matches!(ev, Event::Resync { .. } | Event::Overflow { .. })
44}
45
46fn event_ts(ev: &Event) -> Timestamps {
47    let ts = match ev {
48        Event::OrderBook(brokapi_core::OrderBook::Snapshot { ts, .. })
49        | Event::OrderBook(brokapi_core::OrderBook::Increment { ts, .. }) => Some(*ts),
50        Event::MarketTrade(t) => Some(t.ts),
51        Event::Quote(q) => Some(q.ts),
52        Event::Candle(c) => Some(c.ts),
53        Event::Order(o) => Some(o.ts),
54        Event::Execution(e) => Some(e.ts),
55        Event::Portfolio(p) => Some(p.ts),
56        Event::Positions(v) => v.first().map(|p| p.ts),
57        Event::Balances(v) => v.first().map(|b| b.ts),
58        _ => None,
59    };
60    ts.unwrap_or_else(|| Timestamps::new(now_nanos(), brokapi_runtime::now()))
61}
62
63struct Egress {
64    source: Arc<str>,
65    control: VecDeque<Envelope>,
66    ready: VecDeque<SubscriptionId>,
67    overflowed: HashSet<SubscriptionId>,
68}
69
70impl Egress {
71    fn envelope(&self, sub: Option<SubscriptionId>, payload: Event) -> Envelope {
72        Envelope::new(Arc::clone(&self.source), event_ts(&payload), payload).with_sub_opt(sub)
73    }
74
75    fn pending(&self) -> bool {
76        !self.control.is_empty() || !self.ready.is_empty()
77    }
78
79    /// Next envelope in delivery order: control first, then buffered data.
80    fn next(&mut self, bp: &SharedBackpressure) -> Option<Envelope> {
81        if let Some(e) = self.control.pop_front() {
82            return Some(e);
83        }
84        while let Some(sub) = self.ready.pop_front() {
85            let ev = bp.lock().expect("backpressure poisoned").pop_ready(sub);
86            if let Some(ev) = ev {
87                return Some(self.envelope(Some(sub), ev));
88            }
89        }
90        None
91    }
92
93    fn ingest(
94        &mut self,
95        ev: CoreEvent,
96        bp: &SharedBackpressure,
97        on_overflow: &(dyn Fn(SubscriptionId) + Send + Sync),
98    ) {
99        let CoreEvent { sub, payload } = ev;
100        let Some(sub) = sub else {
101            let env = self.envelope(None, payload);
102            self.control.push_back(env);
103            return;
104        };
105        if is_control(&payload) {
106            // A `Resync` says "drop what you hold". What is still waiting in the buffer of this
107            // subscription was produced before it, so it goes out first, or the consumer would apply
108            // stale data after it had started over. An `Overflow` is different: the buffer is full of
109            // events that are already lost for the consumer's purposes, the notice goes out at once.
110            if matches!(payload, Event::Resync { .. }) {
111                loop {
112                    let next = bp.lock().expect("backpressure poisoned").pop_ready(sub);
113                    let Some(ev) = next else { break };
114                    let env = self.envelope(Some(sub), ev);
115                    self.control.push_back(env);
116                }
117            }
118            let env = self.envelope(Some(sub), payload);
119            self.control.push_back(env);
120            return;
121        }
122        let lost_from = now_nanos();
123        let result = bp
124            .lock()
125            .expect("backpressure poisoned")
126            .ingest(sub, payload, lost_from);
127        match result {
128            PushResult::Delivered | PushResult::Conflated => {
129                // Accepted again: the overflow was cleared (reconcile).
130                self.overflowed.remove(&sub);
131                self.ready.push_back(sub);
132            }
133            PushResult::DroppedNewest => {}
134            PushResult::Overflow => {
135                if self.overflowed.insert(sub) {
136                    warn!(?sub, "lossless subscription overflowed: connector degraded");
137                    let env = self.envelope(Some(sub), Event::Overflow { sub, lost_from });
138                    self.control.push_back(env);
139                    on_overflow(sub);
140                }
141            }
142        }
143    }
144}
145
146/// Runs until cancelled, the frame channel closes, or the consumer drops the receiver.
147///
148/// `handle` turns one frame into core events (it owns the core, so parsing happens right here in
149/// the task that received the frame). `on_overflow` is called once when a `Lossless` buffer
150/// overflows. The user data connector uses it to mark itself degraded.
151///
152/// Delivery order: control events (`Resync`, `Overflow`) first, then buffered data, one
153/// subscription after another. A `Resync` does not overtake the data of its own subscription that is
154/// already buffered: that data goes out just before it. The pump never waits for a slow consumer inside the core: events wait
155/// in the subscription's buffer, and what the buffer's policy says happens when it is full (merge for
156/// `Conflate`, overflow for `Lossless`).
157///
158/// When the pump is cancelled it makes one last pass and hands over what fits into `out` without
159/// waiting. The rest is dropped.
160///
161/// # Examples
162///
163/// A frame goes in, an envelope with the source label comes out:
164///
165/// ```
166/// use std::sync::{Arc, Mutex};
167/// use std::time::{Duration, Instant};
168/// use brokapi_core::{Event, ResyncReason, SubscriptionId};
169/// use brokapi_finam::core::{MdFrame, MdEvent};
170/// use brokapi_finam::pump::{PumpIo, run_pump};
171/// use brokapi_runtime::BackpressureManager;
172/// use brokapi_sansio::CoreEvent;
173/// use tokio::sync::mpsc;
174/// use tokio_util::sync::CancellationToken;
175///
176/// # #[tokio::main(flavor = "current_thread")]
177/// # async fn main() {
178/// let (frames, rx) = mpsc::channel::<MdFrame>(8);
179/// let (out, mut envelopes) = mpsc::channel(8);
180/// let cancel = CancellationToken::new();
181/// let io = PumpIo {
182///     rx,
183///     out,
184///     source: Arc::from("finam-md"),
185///     backpressure: Arc::new(Mutex::new(BackpressureManager::new())),
186///     cancel: cancel.clone(),
187/// };
188/// // The handler stands in for the connector's core: every frame becomes one control event.
189/// let sub = SubscriptionId::new(1);
190/// let handler = move |_frame: MdFrame, _now: Instant| {
191///     vec![CoreEvent {
192///         sub: Some(sub),
193///         payload: Event::Resync {
194///             sub,
195///             instrument: brokapi_finam::util::instrument_from_symbol("SBER@MISX"),
196///             reason: ResyncReason::SubscriptionReset,
197///         },
198///     }]
199/// };
200/// let pump = tokio::spawn(run_pump(io, handler, Arc::new(|_| {})));
201///
202/// frames.send(MdFrame { sub, event: MdEvent::Connected }).await.unwrap();
203/// let envelope = tokio::time::timeout(Duration::from_secs(5), envelopes.recv()).await.unwrap().unwrap();
204/// assert_eq!(&*envelope.source, "finam-md");
205/// assert!(matches!(envelope.payload, Event::Resync { .. }));
206///
207/// cancel.cancel();
208/// pump.await.unwrap();
209/// # }
210/// ```
211pub async fn run_pump<F, H>(
212    io: PumpIo<F>,
213    mut handle: H,
214    on_overflow: Arc<dyn Fn(SubscriptionId) + Send + Sync>,
215) where
216    F: Send + 'static,
217    H: FnMut(F, Instant) -> Vec<CoreEvent> + Send,
218{
219    let PumpIo {
220        mut rx,
221        out,
222        source,
223        backpressure,
224        cancel,
225    } = io;
226    let mut eg = Egress {
227        source,
228        control: VecDeque::new(),
229        ready: VecDeque::new(),
230        overflowed: HashSet::new(),
231    };
232    loop {
233        // Hand over whatever fits without waiting.
234        while eg.pending() {
235            match out.try_reserve() {
236                Ok(permit) => match eg.next(&backpressure) {
237                    Some(env) => permit.send(env),
238                    None => break,
239                },
240                Err(mpsc::error::TrySendError::Full(_)) => break,
241                Err(mpsc::error::TrySendError::Closed(_)) => return,
242            }
243        }
244        let want_send = eg.pending();
245        tokio::select! {
246            biased;
247            _ = cancel.cancelled() => break,
248            permit = out.reserve(), if want_send => match permit {
249                Ok(permit) => {
250                    if let Some(env) = eg.next(&backpressure) {
251                        permit.send(env);
252                    }
253                }
254                Err(_) => return,
255            },
256            frame = rx.recv() => match frame {
257                Some(f) => {
258                    let now = brokapi_runtime::now();
259                    for ev in handle(f, now) {
260                        eg.ingest(ev, &backpressure, on_overflow.as_ref());
261                    }
262                }
263                None => break,
264            },
265        }
266    }
267    // Last chance for what is already buffered.
268    while eg.pending() {
269        match out.try_reserve() {
270            Ok(permit) => match eg.next(&backpressure) {
271                Some(env) => permit.send(env),
272                None => break,
273            },
274            Err(_) => {
275                debug!("pump stopped with undelivered events (consumer not draining)");
276                break;
277            }
278        }
279    }
280}