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