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}