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