Skip to main content

pipecrab_runtime/
inbound.rs

1//! Each stage has an Inbound mailbox with two typed lanes:
2//! `sys` — the priority lane, drains first, carries `(Direction, SystemFrame)`.
3//! `data` — the data lane, carries bare `DataFrame` (downstream only).
4//!
5//! Keeping the lanes typed prevents misrouting a media frame onto the system
6//! lane and removes the per-frame is-system check from the hot path.
7//!
8//! Every frame crosses its link carrying a per-link sequence stamp (see
9//! [`Stamped`]), which makes the interrupt flush *causal*: a flush drops only
10//! frames queued before the system frame it flushes against, so a barge-in
11//! utterance sent behind its own `Interrupt` is never destroyed by it.
12
13use futures::channel::mpsc::Receiver;
14use futures::stream::StreamExt;
15use pipecrab_core::{DataFrame, Direction, SystemFrame};
16
17/// A frame paired with the sequence stamp its link's
18/// [`Outbound`](crate::Outbound) applied.
19///
20/// Both lanes of one link share a single monotonic counter, so stamps order
21/// frames *across* lanes: [`Inbound::flush_data`] keeps any data frame stamped
22/// at or after the system frame being flushed against.
23#[derive(Debug)]
24pub(crate) struct Stamped<T> {
25    /// Per-link monotonic sequence number; `0` is never issued.
26    pub(crate) seq: u64,
27    /// The carried frame.
28    pub(crate) frame: T,
29}
30
31/// A frame received from [`Inbound::recv`]: either a system frame (with its
32/// travel direction) or a data frame (always downstream).
33#[derive(Debug)]
34pub enum Received {
35    /// A system frame and the direction it is travelling.
36    Sys(Direction, SystemFrame),
37    /// A data frame, implicitly travelling downstream.
38    Data(DataFrame),
39}
40
41/// The receive surface of a stage: a preempting system lane and the data lane.
42///
43/// Within a lane, frames keep FIFO order. Across lanes, `sys` always wins, so a
44/// system frame is taken even when `data` is backed up.
45///
46/// Constructed only by [`link`](crate::link); the lanes are private so every
47/// receive goes through [`recv`](Self::recv), which maintains the flush floor
48/// [`flush_data`](Self::flush_data) relies on.
49pub struct Inbound {
50    /// System-tier frames (lifecycle, interruption, errors). Drained first.
51    /// `Error` rides this lane *upstream*; `Interrupt`/`Start`/`Stop` ride it
52    /// downstream. Sparse and latency-critical.
53    pub(crate) sys: Receiver<Stamped<(Direction, SystemFrame)>>,
54    /// Data-tier frames (media, transcripts), in FIFO order, downstream only.
55    pub(crate) data: Receiver<Stamped<DataFrame>>,
56    /// Stamp of the most recent system frame taken off `sys` — the floor
57    /// [`flush_data`](Self::flush_data) flushes up to.
58    pub(crate) flush_floor: u64,
59}
60
61impl Inbound {
62    /// Receive the next frame, draining the system lane before the data lane.
63    ///
64    /// Returns [`Received::Sys`] or [`Received::Data`], or `None` once *both*
65    /// lanes are closed — the run-loop's shutdown signal.
66    ///
67    /// [`futures::select_biased`] polls `sys` first, so a system frame preempts
68    /// any data backlog deterministically. When a lane closes, its receiver
69    /// (a [`FusedStream`]) yields `None`; the `loop` swallows that first `None`
70    /// so the next iteration just skips the dead lane instead of treating it as
71    /// shutdown. This is so the sys lane can keep draining even after the data
72    /// lane shuts down — `None` is returned only once *both* lanes have closed.
73    ///
74    /// [`FusedStream`]: futures::stream::FusedStream
75    pub async fn recv(&mut self) -> Option<Received> {
76        loop {
77            futures::select_biased! {
78                sys = self.sys.next() => {
79                    if let Some(Stamped { seq, frame: (dir, f) }) = sys {
80                        self.flush_floor = seq;
81                        return Some(Received::Sys(dir, f));
82                    }
83                }
84                data = self.data.next() => {
85                    if let Some(Stamped { frame, .. }) = data {
86                        return Some(Received::Data(frame));
87                    }
88                }
89                complete => return None,
90            }
91        }
92    }
93
94    /// Drain everything currently queued on the data lane. A frame queued
95    /// *before* the most recently received system frame is kept only if
96    /// `survives_flush()`; a frame queued at or after it is always kept.
97    /// Keepers are returned in arrival order, for the caller to re-process.
98    /// Does not block and does not touch the sys lane.
99    ///
100    /// Only meaningful straight after receiving the system frame to flush
101    /// against — receiving another system frame moves the floor.
102    pub fn flush_data(&mut self) -> Vec<DataFrame> {
103        self.flush_data_stamped()
104            .into_iter()
105            .map(|stamped| stamped.frame)
106            .collect()
107    }
108
109    /// [`flush_data`](Self::flush_data), keeping the stamps: the run loop holds
110    /// keepers across a later interrupt, whose flush must re-judge them by seq.
111    pub(crate) fn flush_data_stamped(&mut self) -> Vec<Stamped<DataFrame>> {
112        let mut kept = Vec::new();
113        while let Ok(stamped) = self.data.try_recv() {
114            if stamped.seq >= self.flush_floor || stamped.frame.survives_flush() {
115                kept.push(stamped);
116            }
117        }
118        kept
119    }
120
121    /// Await the next system frame, ignoring the data lane and maintaining the
122    /// flush floor exactly as [`recv`](Self::recv) does.
123    ///
124    /// `None` once the system lane closes, and immediately so from then on — a
125    /// caller racing this against other work must stop polling it at that
126    /// point. Cancellation-safe: a dropped future takes nothing off the lane.
127    ///
128    /// This is what keeps an application's output pump preemptible while it is
129    /// busy with a data frame. A stage gets sys priority from the run loop's
130    /// own race; the tail lane belongs to the application, so its pump must run
131    /// the same race itself (see the e2e examples' `pump_out`).
132    pub async fn recv_sys(&mut self) -> Option<(Direction, SystemFrame)> {
133        let Stamped {
134            seq,
135            frame: (dir, frame),
136        } = self.sys.next().await?;
137        self.flush_floor = seq;
138        Some((dir, frame))
139    }
140
141    /// Take one already-queued system frame without blocking, maintaining the
142    /// flush floor exactly as [`recv`](Self::recv) would. `None` when the sys
143    /// lane is empty or closed. Lets the run loop keep sys priority while it
144    /// replays flush keepers instead of awaiting `recv`.
145    pub(crate) fn try_recv_sys(&mut self) -> Option<(Direction, SystemFrame)> {
146        match self.sys.try_recv() {
147            Ok(Stamped {
148                seq,
149                frame: (dir, frame),
150            }) => {
151                self.flush_floor = seq;
152                Some((dir, frame))
153            }
154            Err(_) => None,
155        }
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    //! Lane-close semantics need one lane to close while the other stays open,
162    //! which the public [`link`](crate::link) surface cannot express (one
163    //! `Outbound` owns both senders) — so these live here, on the raw lanes.
164
165    use futures::FutureExt;
166    use futures::channel::mpsc;
167    use futures::executor::block_on;
168    use pipecrab_core::Transcript;
169
170    use super::*;
171
172    #[allow(clippy::type_complexity)]
173    fn lanes() -> (
174        mpsc::Sender<Stamped<(Direction, SystemFrame)>>,
175        mpsc::Sender<Stamped<DataFrame>>,
176        Inbound,
177    ) {
178        let (sys_tx, sys) = mpsc::channel(16);
179        let (data_tx, data) = mpsc::channel(16);
180        (
181            sys_tx,
182            data_tx,
183            Inbound {
184                sys,
185                data,
186                flush_floor: 0,
187            },
188        )
189    }
190
191    #[test]
192    fn both_lanes_closed_yields_none() {
193        block_on(async {
194            let (sys_tx, data_tx, mut inb) = lanes();
195            drop(sys_tx);
196            drop(data_tx);
197            assert!(
198                inb.recv().await.is_none(),
199                "closed lanes must signal shutdown via None"
200            );
201        });
202    }
203
204    #[test]
205    fn one_closed_lane_does_not_signal_shutdown() {
206        block_on(async {
207            let (sys_tx, data_tx, mut inb) = lanes();
208            // Data lane closes while sys is still open but empty.
209            drop(data_tx);
210            // recv must NOT resolve to None — a single closed lane is not
211            // shutdown. `now_or_never` yields `None` while still pending.
212            assert!(
213                inb.recv().now_or_never().is_none(),
214                "a still-open sys lane must keep recv pending, not report shutdown",
215            );
216
217            // The other lane closing too is what finally yields `None`.
218            drop(sys_tx);
219            assert!(
220                matches!(inb.recv().now_or_never(), Some(None)),
221                "both lanes closed must resolve immediately to None",
222            );
223        });
224    }
225
226    #[test]
227    fn closed_sys_lane_still_serves_buffered_data() {
228        block_on(async {
229            let (sys_tx, mut data_tx, mut inb) = lanes();
230            data_tx
231                .try_send(Stamped {
232                    seq: 1,
233                    frame: Transcript::user_final("after sys closed").into(),
234                })
235                .unwrap();
236            // Sys lane closes, but a buffered data frame must still be
237            // delivered.
238            drop(sys_tx);
239
240            match inb.recv().await.unwrap() {
241                Received::Data(DataFrame::Transcript(s)) => {
242                    assert_eq!(s.text, "after sys closed".into())
243                }
244                other => panic!("closed sys lane must not block the data lane, got {other:?}"),
245            }
246        });
247    }
248
249    #[test]
250    fn recv_sys_takes_the_system_frame_past_a_backed_up_data_lane() {
251        block_on(async {
252            let (mut sys_tx, mut data_tx, mut inb) = lanes();
253            data_tx
254                .try_send(Stamped {
255                    seq: 1,
256                    frame: Transcript::user_final("stale").into(),
257                })
258                .unwrap();
259            sys_tx
260                .try_send(Stamped {
261                    seq: 2,
262                    frame: (Direction::Down, SystemFrame::Interrupt),
263                })
264                .unwrap();
265            data_tx
266                .try_send(Stamped {
267                    seq: 3,
268                    frame: Transcript::user_final("barge-in").into(),
269                })
270                .unwrap();
271
272            assert!(
273                matches!(
274                    inb.recv_sys().await,
275                    Some((Direction::Down, SystemFrame::Interrupt))
276                ),
277                "recv_sys must reach the system frame without draining data first",
278            );
279            // The floor moved, so the causal flush still discriminates.
280            let kept = inb.flush_data();
281            assert_eq!(kept.len(), 1, "only the post-Interrupt frame survives");
282            match &kept[0] {
283                DataFrame::Transcript(s) => assert_eq!(s.text, "barge-in".into()),
284                other => panic!("wrong survivor: {other:?}"),
285            }
286        });
287    }
288
289    #[test]
290    fn recv_sys_reports_a_closed_sys_lane_and_leaves_data_alone() {
291        block_on(async {
292            let (sys_tx, mut data_tx, mut inb) = lanes();
293            data_tx
294                .try_send(Stamped {
295                    seq: 1,
296                    frame: Transcript::user_final("still here").into(),
297                })
298                .unwrap();
299            drop(sys_tx);
300
301            assert!(
302                matches!(inb.recv_sys().now_or_never(), Some(None)),
303                "a closed sys lane must resolve immediately, so a racing caller can stop polling",
304            );
305            match inb.recv().await.unwrap() {
306                Received::Data(DataFrame::Transcript(s)) => {
307                    assert_eq!(s.text, "still here".into())
308                }
309                other => panic!("recv_sys must not consume data frames, got {other:?}"),
310            }
311        });
312    }
313}