Skip to main content

pipecrab_runtime/
stage.rs

1//! The [`Stage`] trait: the async, effecting half of a pipeline stage, and the
2//! preemptible run loop ([`Stage::run`]) that drives one.
3//!
4//! A stage is a [`Processor`](pipecrab_core::Processor) — synchronous,
5//! state-owning `decide_*` — plus an
6//! async [`Stage::perform`] that interprets the effects `decide_*` emitted and
7//! does the actual I/O. The split is the core invariant: `decide_*` takes
8//! `&mut self` and is the *only* place state changes; `perform` takes `&self`
9//! and must never mutate state, so the run loop can drop an in-flight `perform`
10//! future on an interrupt without leaving torn state behind.
11//!
12//! [`Stage::run`] ties a stage to an [`Inbound`] and an [`Outbound`] and drives
13//! it. Its default body is the leaf run loop; a composite stage (a
14//! [`Pipeline`](crate::Pipeline)) overrides it to drive its children — which is
15//! why a pipeline is itself a `Stage` and can nest.
16
17use std::collections::VecDeque;
18use std::fmt;
19use std::sync::Arc;
20
21use async_trait::async_trait;
22use futures::future::FutureExt;
23use futures::pin_mut;
24use futures::stream::StreamExt;
25use pipecrab_core::{DataFrame, Direction, Disposition, Processor, SystemFrame};
26
27use crate::inbound::Stamped;
28use crate::{Inbound, MaybeSend, MaybeSendSync, Outbound, Received};
29
30/// Why a [`Stage::perform`] call failed.
31///
32/// `perform` is the fallible, I/O-doing half of a stage. The run loop surfaces
33/// a returned error as a `SystemFrame::Error` travelling upstream; `fatal`
34/// decides whether the pipeline should tear down rather than carry on.
35///
36/// Mirrors the shape of `SystemFrame::Error` (a message plus a `fatal` flag) so
37/// the conversion at the run-loop boundary is direct.
38#[derive(Debug, Clone)]
39pub struct StageError {
40    /// Human-readable description of what went wrong.
41    pub message: Arc<str>,
42    /// Whether the failure is unrecoverable and the pipeline should shut down.
43    pub fatal: bool,
44}
45
46impl StageError {
47    /// A recoverable error: the pipeline may keep running.
48    pub fn new(message: impl Into<Arc<str>>) -> Self {
49        Self {
50            message: message.into(),
51            fatal: false,
52        }
53    }
54
55    /// An unrecoverable error: the pipeline should shut down.
56    pub fn fatal(message: impl Into<Arc<str>>) -> Self {
57        Self {
58            message: message.into(),
59            fatal: true,
60        }
61    }
62}
63
64impl fmt::Display for StageError {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        let kind = if self.fatal {
67            "fatal stage error"
68        } else {
69            "stage error"
70        };
71        write!(f, "{kind}: {}", self.message)
72    }
73}
74
75impl std::error::Error for StageError {}
76
77impl From<String> for StageError {
78    fn from(message: String) -> Self {
79        Self::new(message)
80    }
81}
82
83impl From<&str> for StageError {
84    fn from(message: &str) -> Self {
85        Self::new(message)
86    }
87}
88
89/// The async, effecting half of a pipeline stage.
90///
91/// `Stage` extends [`Processor`]: `decide_data` / `decide_system` (synchronous,
92/// `&mut self`) own all state mutation and emit [`Effect`](Processor::Effect)
93/// values; [`perform`](Stage::perform) interprets one effect, does its I/O, and
94/// pushes any resulting frames through `out`.
95///
96/// [`run`](Stage::run) drives the stage given an [`Inbound`] and an
97/// [`Outbound`]. Its default is the preemptible leaf loop; a composite stage
98/// overrides it (see [`Pipeline`](crate::Pipeline)), which is what lets a
99/// pipeline be a `Stage` and nest inside another.
100///
101/// # `?Send` is deliberate
102///
103/// pipecrab commits to a single-threaded execution model, so the returned
104/// futures are **not** required to be `Send`. One `Stage` definition then runs
105/// unchanged both on a tokio current-thread runtime and in the browser
106/// (`wasm32`), where `Send` bounds are impossible to satisfy. CPU-bound or
107/// blocking work must not run inline on the orchestrator thread — push it
108/// off-thread with [`offload`](fn@crate::offload) and `await` the result, so an
109/// interrupt can still preempt `perform` promptly.
110///
111/// The trait is dyn-compatible (via `async_trait`). A pipeline erases the
112/// associated effect type at insertion and stores only an object-safe runner,
113/// allowing stages with different effect types to compose.
114#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
115#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
116pub trait Stage: Processor + MaybeSendSync
117where
118    Self::Effect: MaybeSend,
119{
120    /// Interpret one effect emitted by `decide_*` and carry out its I/O, sending
121    /// any resulting frames through `out`.
122    ///
123    /// Takes `&self`: `perform` must not mutate stage state. The run loop races
124    /// this future against the system lane, so a barge-in `Interrupt` can drop
125    /// it mid-flight; because only `decide_*` ever mutated state, dropping the
126    /// future leaves the stage intact. Barge-in is only as responsive as
127    /// `perform` yields, so never block the thread inline — [`offload`] heavy
128    /// work and `await` it.
129    ///
130    /// [`offload`]: fn@crate::offload
131    async fn perform(&self, effect: Self::Effect, out: &Outbound) -> Result<(), StageError>;
132
133    /// Drive this stage to completion: consume frames from `inbound`, emit
134    /// through `out`, return once `inbound` closes (or on `Stop` / a fatal
135    /// error).
136    ///
137    /// The default is the preemptible run loop. System frames are drained
138    /// before data (via [`Inbound::recv`]). While a data frame's effects run in
139    /// `perform`, the system lane is raced against them: an `Interrupt` drops
140    /// the in-flight `perform` immediately; any other system frame is *stashed*
141    /// and handled once `perform` is dropped — we cannot call the `&mut self`
142    /// `decide_system` while `perform` borrows `&self`, so the stash defers it
143    /// until that borrow ends.
144    ///
145    /// After an `Interrupt` is handled, the queued data backlog is flushed via
146    /// [`Inbound::flush_data`]: droppable frames queued before the `Interrupt`
147    /// are discarded; survivors and frames queued after it are kept and
148    /// re-processed ahead of the next inbound read, so a barge-in utterance is
149    /// not clipped. The replay itself yields to any system frame already
150    /// queued — the sys lane keeps its priority — and a later interrupt
151    /// re-judges the held keepers by their stamps.
152    ///
153    /// A composite stage overrides this; the default body is never invoked for
154    /// one (see [`Pipeline`](crate::Pipeline)).
155    async fn run(self: Box<Self>, inbound: Inbound, out: Outbound) {
156        let mut stage = self;
157        let mut inbound = inbound;
158        // Keepers of an interrupt flush, re-processed ahead of the next read.
159        // Stamped so a later interrupt's flush can re-judge them by seq.
160        let mut pending: VecDeque<Stamped<DataFrame>> = VecDeque::new();
161        loop {
162            let received = if pending.is_empty() {
163                match inbound.recv().await {
164                    Some(received) => received,
165                    None => break,
166                }
167            } else {
168                // Replaying keepers must not starve the sys lane: a system
169                // frame already queued keeps the priority recv() would give it.
170                match inbound.try_recv_sys() {
171                    Some((dir, frame)) => Received::Sys(dir, frame),
172                    None => Received::Data(pending.pop_front().expect("non-empty").frame),
173                }
174            };
175            match received {
176                Received::Sys(dir, frame) => {
177                    let interrupted = matches!(frame, SystemFrame::Interrupt);
178                    let stop = handle_system(&mut *stage, dir, frame, &out).await;
179                    if interrupted {
180                        // Barge-in: discard the stale queued data backlog, but
181                        // keep survivors and anything queued after the
182                        // Interrupt, re-processing them so the new utterance is
183                        // not clipped. Held keepers are re-judged the same way.
184                        let floor = inbound.flush_floor;
185                        pending.retain(|s| s.seq >= floor || s.frame.survives_flush());
186                        pending.extend(inbound.flush_data_stamped());
187                    }
188                    if stop {
189                        break;
190                    }
191                }
192                Received::Data(frame) => {
193                    let decision = stage.decide_data(&frame);
194                    if decision.disposition == Disposition::Forward {
195                        let _ = out.send_data(frame).await;
196                    }
197                    if decision.effects.is_empty() {
198                        continue;
199                    }
200
201                    let mut stashed: Vec<(Direction, SystemFrame)> = Vec::new();
202                    let mut interrupt: Option<(u64, Direction, SystemFrame)> = None;
203                    let mut should_stop = false;
204                    {
205                        // `perform` borrows `&*stage` for its whole lifetime, so
206                        // no `&mut *stage` (i.e. no `decide_system`) is possible
207                        // until it is dropped at the end of this block.
208                        let perform = run_effects(&*stage, decision.effects, &out).fuse();
209                        pin_mut!(perform);
210                        loop {
211                            futures::select_biased! {
212                                maybe = inbound.sys.next() => {
213                                    // `None` => sys lane closed; keep performing.
214                                    if let Some(Stamped { seq, frame: (d, f) }) = maybe {
215                                        if matches!(f, SystemFrame::Interrupt) {
216                                            interrupt = Some((seq, d, f));
217                                            break; // drops `perform`: barge-in
218                                        }
219                                        stashed.push((d, f)); // defer; keep performing
220                                    }
221                                },
222                                res = perform => {
223                                    if let Err(e) = res {
224                                        let fatal = e.fatal;
225                                        emit_error(&out, e).await;
226                                        should_stop |= fatal;
227                                    }
228                                    break;
229                                },
230                                complete => break,
231                            }
232                        }
233                    }
234
235                    // `perform` is dropped; `&mut *stage` is free again.
236                    for (d, f) in stashed.drain(..) {
237                        should_stop |= handle_system(&mut *stage, d, f, &out).await;
238                    }
239                    if let Some((seq, d, f)) = interrupt {
240                        // This path took the frame straight off the sys lane,
241                        // so record the floor `recv` would have.
242                        inbound.flush_floor = seq;
243                        should_stop |= handle_system(&mut *stage, d, f, &out).await;
244                        // Same barge-in flush as the outer Sys branch.
245                        pending.retain(|s| s.seq >= seq || s.frame.survives_flush());
246                        pending.extend(inbound.flush_data_stamped());
247                    }
248                    if should_stop {
249                        break;
250                    }
251                }
252            }
253        }
254    }
255}
256
257/// Run a system frame through the stage: `decide_system`, forward on `Forward`,
258/// then perform its effects. Returns `true` if the stage should stop (the frame
259/// was a `Stop`, or an effect failed fatally).
260async fn handle_system<S: Stage + ?Sized>(
261    stage: &mut S,
262    dir: Direction,
263    frame: SystemFrame,
264    out: &Outbound,
265) -> bool
266where
267    S::Effect: MaybeSend,
268{
269    let mut should_stop = matches!(frame, SystemFrame::Stop);
270    let decision = stage.decide_system(dir, &frame);
271    if decision.disposition == Disposition::Forward {
272        let _ = out.send_system(dir, frame).await;
273    }
274    for effect in decision.effects {
275        if let Err(e) = stage.perform(effect, out).await {
276            let fatal = e.fatal;
277            emit_error(out, e).await;
278            should_stop |= fatal;
279        }
280    }
281    should_stop
282}
283
284/// Perform a stage's effects in order, short-circuiting on the first error.
285async fn run_effects<S: Stage + ?Sized>(
286    stage: &S,
287    effects: Vec<S::Effect>,
288    out: &Outbound,
289) -> Result<(), StageError>
290where
291    S::Effect: MaybeSend,
292{
293    for effect in effects {
294        stage.perform(effect, out).await?;
295    }
296    Ok(())
297}
298
299/// Surface a `perform` failure as an `Error` system frame. v1 sends it on the
300/// downstream `sys` lane tagged [`Direction::Up`]; true upstream routing is a
301/// follow-up.
302async fn emit_error(out: &Outbound, e: StageError) {
303    let _ = out
304        .send_system(
305            Direction::Up,
306            SystemFrame::Error {
307                message: e.message,
308                fatal: e.fatal,
309            },
310        )
311        .await;
312}