Skip to main content

dynamo_runtime/pipeline/
nodes.rs

1// SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Pipeline Nodes
5//!
6//! A `ServicePipeline` is a directed graph of nodes where each node defines a behavior for both
7//! forward/request path and the backward/response path. The allowed behaviors in each direction
8//! are is either a `Source`, or a `Sink`.
9//!
10//! A `Frontend` is a the start of a graph and is a [`Source`] for the forward path and a [`Sink`] for the
11//! backward path.
12//!
13//! A `Backend` is the end of a graph and is a [`Sink`] for the forward path and a [`Source`] for the
14//! backward path.
15//!
16//! An [`PipelineOperator`] is a node that can transform both the forward and backward paths using the
17//! logic supplied by the implementation of an [`Operator`] trait. Because the [`PipelineOperator`] is
18//! both a [`Source`] and a [`Sink`] of the forward request path and the backward response path respectively,
19//! i.e. it is two sources and two sinks. We can differentiate the two by using the [`PipelineOperator::forward_edge`]
20//! and [`PipelineOperator::backward_edge`] methods.
21//!
22//! - The [`PipelineOperator::forward_edge`] returns a [`PipelineOperatorForwardEdge`] which is a [`Sink`]
23//!   for incoming/upstream request and a [`Source`] for the downstream request.
24//! - The [`PipelineOperator::backward_edge`] returns a [`PipelineOperatorBackwardEdge`] which is a [`Sink`]
25//!   for the downstream response and a [`Source`] for the upstream response.
26//!
27//! An `EdgeOperator` currently named [`PipelineNode`] is a node in the graph can transform only a forward
28//! or a backward path, but does not transform both.
29//!
30//! This makes the [`Operator`] a more powerful trait as it can propagate information from the forward
31//! path to the backward path. An `EdgeOperator` on the forward path has no visibility into the backward
32//! path and therefore, cannot directly influence the backward path.
33//!
34use std::{
35    collections::HashMap,
36    sync::{Arc, Mutex, OnceLock, Weak},
37};
38
39use super::{AsyncEngine, AsyncEngineContextProvider};
40use async_trait::async_trait;
41use tokio::sync::oneshot;
42
43use super::{Data, Error, PipelineError, PipelineIO};
44
45mod sinks;
46mod sources;
47
48pub use sinks::{SegmentSink, ServiceBackend};
49pub use sources::{SegmentSource, ServiceFrontend};
50
51pub type Service<In, Out> = Arc<ServiceFrontend<In, Out>>;
52
53mod private {
54    pub struct Token;
55}
56
57// todo rename `ServicePipelineExt`
58/// A [`Source`] trait defines how data is emitted from a source to a downstream sink.
59#[async_trait]
60pub trait Source<T: PipelineIO>: Data {
61    async fn on_next(&self, data: T, _: private::Token) -> Result<(), Error>;
62
63    fn set_edge(&self, edge: Edge<T>, _: private::Token) -> Result<(), PipelineError>;
64
65    fn link<S: Sink<T> + 'static>(&self, sink: Arc<S>) -> Result<Arc<S>, PipelineError> {
66        let edge = Edge::new(sink.clone());
67        self.set_edge(edge, private::Token)?;
68        Ok(sink)
69    }
70
71    /// Close a pipeline response path without creating an ownership cycle.
72    ///
73    /// The returned sink is the pipeline's ownership root and must be retained by the caller.
74    fn link_terminal<S: Sink<T> + 'static>(&self, sink: Arc<S>) -> Result<Arc<S>, PipelineError> {
75        let edge = Edge::new_weak(sink.clone());
76        self.set_edge(edge, private::Token)?;
77        Ok(sink)
78    }
79}
80
81/// A [`Sink`] trait defines how data is received from a source and processed.
82#[async_trait]
83pub trait Sink<T: PipelineIO>: Data {
84    async fn on_data(&self, data: T, _: private::Token) -> Result<(), Error>;
85}
86
87/// An [`Edge`] is a connection between a [`Source`] and a [`Sink`].
88pub struct Edge<T: PipelineIO> {
89    downstream: EdgeTarget<T>,
90}
91
92enum EdgeTarget<T: PipelineIO> {
93    Strong(Arc<dyn Sink<T>>),
94    Weak(Weak<dyn Sink<T>>),
95}
96
97impl<T: PipelineIO> Edge<T> {
98    fn new(downstream: Arc<dyn Sink<T>>) -> Self {
99        Edge {
100            downstream: EdgeTarget::Strong(downstream),
101        }
102    }
103
104    fn new_weak<S: Sink<T> + 'static>(downstream: Arc<S>) -> Self {
105        let downstream: Arc<dyn Sink<T>> = downstream;
106        Edge {
107            downstream: EdgeTarget::Weak(Arc::downgrade(&downstream)),
108        }
109    }
110
111    async fn write(&self, data: T) -> Result<(), Error> {
112        match &self.downstream {
113            EdgeTarget::Strong(downstream) => downstream.on_data(data, private::Token).await,
114            EdgeTarget::Weak(downstream) => {
115                let Some(downstream) = downstream.upgrade() else {
116                    data.context().stop_generating();
117                    return Err(PipelineError::DetachedStreamReceiver.into());
118                };
119                downstream.on_data(data, private::Token).await
120            }
121        }
122    }
123}
124
125type NodeFn<In, Out> = Box<dyn Fn(In) -> Result<Out, Error> + Send + Sync>;
126
127/// An [`Operator`] is a trait that defines the behavior of how two [`AsyncEngine`] can be chained together.
128/// An [`Operator`] is not quite an [`AsyncEngine`] because its generate method requires both the upstream
129/// request, but also the downstream [`AsyncEngine`] to which it will pass the transformed request.
130/// The [`Operator`] logic must transform the upstream request `UpIn` to the downstream request `DownIn`,
131/// then transform the downstream response `DownOut` to the upstream response `UpOut`.
132///
133/// A [`PipelineOperator`] accepts an [`Operator`] and presents itself as an [`AsyncEngine`] for the upstream
134/// [`AsyncEngine<UpIn, UpOut, Error>`].
135///
136/// ### Example of type transformation and data flow
137/// ```text
138/// ... --> <UpIn> ---> [Operator] --> <DownIn> ---> ...
139/// ... <-- <UpOut> --> [Operator] <-- <DownOut> <-- ...
140/// ```
141#[async_trait]
142pub trait Operator<UpIn: PipelineIO, UpOut: PipelineIO, DownIn: PipelineIO, DownOut: PipelineIO>:
143    Data
144{
145    /// This method is expected to transform the upstream request `UpIn` to the downstream request `DownIn`,
146    /// call the next [`AsyncEngine`] with the transformed request, then transform the downstream response
147    /// `DownOut` to the upstream response `UpOut`.
148    async fn generate(
149        &self,
150        req: UpIn,
151        next: Arc<dyn AsyncEngine<DownIn, DownOut, Error>>,
152    ) -> Result<UpOut, Error>;
153
154    fn into_operator(self: &Arc<Self>) -> Arc<PipelineOperator<UpIn, UpOut, DownIn, DownOut>>
155    where
156        Self: Sized,
157    {
158        PipelineOperator::new(self.clone())
159    }
160}
161
162/// A [`PipelineOperatorForwardEdge`] is [`Sink`] for the upstream request type `UpIn` and a [`Source`] for the
163/// downstream request type `DownIn`.
164pub struct PipelineOperatorForwardEdge<
165    UpIn: PipelineIO,
166    UpOut: PipelineIO,
167    DownIn: PipelineIO,
168    DownOut: PipelineIO,
169> {
170    parent: Arc<PipelineOperator<UpIn, UpOut, DownIn, DownOut>>,
171}
172
173/// A [`PipelineOperatorBackwardEdge`] is [`Sink`] for the downstream response type `DownOut` and a [`Source`] for the
174/// upstream response type `UpOut`.
175pub struct PipelineOperatorBackwardEdge<
176    UpIn: PipelineIO,
177    UpOut: PipelineIO,
178    DownIn: PipelineIO,
179    DownOut: PipelineIO,
180> {
181    parent: Weak<PipelineOperator<UpIn, UpOut, DownIn, DownOut>>,
182}
183
184/// A [`PipelineOperator`] is a node that can transform both the forward and backward paths using the logic defined
185/// by the implementation of an [`Operator`] trait.
186pub struct PipelineOperator<
187    UpIn: PipelineIO,
188    UpOut: PipelineIO,
189    DownIn: PipelineIO,
190    DownOut: PipelineIO,
191> {
192    // core business logic of this object
193    operator: Arc<dyn Operator<UpIn, UpOut, DownIn, DownOut>>,
194
195    // this hold the downstream connections via the generic frontend
196    // frontends provide both a source and a sink interfaces
197    downstream: Arc<sources::Frontend<DownIn, DownOut>>,
198
199    // this hold the connection to the previous/upstream response sink
200    // we are a source to that upstream's response sink
201    upstream: sinks::SinkEdge<UpOut>,
202}
203
204impl<UpIn, UpOut, DownIn, DownOut> PipelineOperator<UpIn, UpOut, DownIn, DownOut>
205where
206    UpIn: PipelineIO,
207    UpOut: PipelineIO,
208    DownIn: PipelineIO,
209    DownOut: PipelineIO,
210{
211    /// Create a new [`PipelineOperator`] with the given [`Operator`] implementation.
212    pub fn new(operator: Arc<dyn Operator<UpIn, UpOut, DownIn, DownOut>>) -> Arc<Self> {
213        Arc::new(PipelineOperator {
214            operator,
215            downstream: Arc::new(sources::Frontend::default()),
216            upstream: sinks::SinkEdge::default(),
217        })
218    }
219
220    /// Access the forward edge of the [`PipelineOperator`] allowing the forward/requests paths to be linked.
221    pub fn forward_edge(
222        self: &Arc<Self>,
223    ) -> Arc<PipelineOperatorForwardEdge<UpIn, UpOut, DownIn, DownOut>> {
224        Arc::new(PipelineOperatorForwardEdge {
225            parent: self.clone(),
226        })
227    }
228
229    /// Access the backward edge of the [`PipelineOperator`] allowing the backward/responses paths to be linked.
230    pub fn backward_edge(
231        self: &Arc<Self>,
232    ) -> Arc<PipelineOperatorBackwardEdge<UpIn, UpOut, DownIn, DownOut>> {
233        Arc::new(PipelineOperatorBackwardEdge {
234            parent: Arc::downgrade(self),
235        })
236    }
237}
238
239/// A [`PipelineOperator`] is an [`AsyncEngine`] for the upstream [`AsyncEngine<UpIn, UpOut, Error>`].
240#[async_trait]
241impl<UpIn, UpOut, DownIn, DownOut> AsyncEngine<UpIn, UpOut, Error>
242    for PipelineOperator<UpIn, UpOut, DownIn, DownOut>
243where
244    UpIn: PipelineIO + Sync,
245    DownIn: PipelineIO + Sync,
246    DownOut: PipelineIO,
247    UpOut: PipelineIO,
248{
249    async fn generate(&self, req: UpIn) -> Result<UpOut, Error> {
250        self.operator.generate(req, self.downstream.clone()).await
251    }
252}
253
254#[async_trait]
255impl<UpIn, UpOut, DownIn, DownOut> Sink<UpIn>
256    for PipelineOperatorForwardEdge<UpIn, UpOut, DownIn, DownOut>
257where
258    UpIn: PipelineIO + Sync,
259    DownIn: PipelineIO + Sync,
260    DownOut: PipelineIO,
261    UpOut: PipelineIO,
262{
263    async fn on_data(&self, data: UpIn, _token: private::Token) -> Result<(), Error> {
264        let stream = self.parent.generate(data).await?;
265        self.parent.upstream.on_next(stream, private::Token).await
266    }
267}
268
269#[async_trait]
270impl<UpIn, UpOut, DownIn, DownOut> Source<DownIn>
271    for PipelineOperatorForwardEdge<UpIn, UpOut, DownIn, DownOut>
272where
273    UpIn: PipelineIO,
274    DownIn: PipelineIO,
275    DownOut: PipelineIO,
276    UpOut: PipelineIO,
277{
278    async fn on_next(&self, data: DownIn, token: private::Token) -> Result<(), Error> {
279        self.parent.downstream.on_next(data, token).await
280    }
281
282    fn set_edge(&self, edge: Edge<DownIn>, token: private::Token) -> Result<(), PipelineError> {
283        self.parent.downstream.set_edge(edge, token)
284    }
285}
286
287#[async_trait]
288impl<UpIn, UpOut, DownIn, DownOut> Sink<DownOut>
289    for PipelineOperatorBackwardEdge<UpIn, UpOut, DownIn, DownOut>
290where
291    UpIn: PipelineIO,
292    DownIn: PipelineIO,
293    DownOut: PipelineIO,
294    UpOut: PipelineIO,
295{
296    async fn on_data(&self, data: DownOut, token: private::Token) -> Result<(), Error> {
297        let Some(parent) = self.parent.upgrade() else {
298            data.context().stop_generating();
299            return Err(PipelineError::DetachedStreamReceiver.into());
300        };
301        parent.downstream.on_data(data, token).await
302    }
303}
304
305#[async_trait]
306impl<UpIn, UpOut, DownIn, DownOut> Source<UpOut>
307    for PipelineOperatorBackwardEdge<UpIn, UpOut, DownIn, DownOut>
308where
309    UpIn: PipelineIO,
310    DownIn: PipelineIO,
311    DownOut: PipelineIO,
312    UpOut: PipelineIO,
313{
314    async fn on_next(&self, data: UpOut, token: private::Token) -> Result<(), Error> {
315        let Some(parent) = self.parent.upgrade() else {
316            data.context().stop_generating();
317            return Err(PipelineError::DetachedStreamReceiver.into());
318        };
319        parent.upstream.on_next(data, token).await
320    }
321
322    fn set_edge(&self, edge: Edge<UpOut>, token: private::Token) -> Result<(), PipelineError> {
323        self.parent
324            .upgrade()
325            .ok_or(PipelineError::DetachedStreamReceiver)?
326            .upstream
327            .set_edge(edge, token)
328    }
329}
330
331pub struct PipelineNode<In: PipelineIO, Out: PipelineIO> {
332    edge: OnceLock<Edge<Out>>,
333    map_fn: NodeFn<In, Out>,
334}
335
336impl<In: PipelineIO, Out: PipelineIO> PipelineNode<In, Out> {
337    pub fn new(map_fn: NodeFn<In, Out>) -> Arc<Self> {
338        Arc::new(PipelineNode::<In, Out> {
339            edge: OnceLock::new(),
340            map_fn,
341        })
342    }
343}
344
345#[async_trait]
346impl<In: PipelineIO, Out: PipelineIO> Source<Out> for PipelineNode<In, Out> {
347    async fn on_next(&self, data: Out, _: private::Token) -> Result<(), Error> {
348        self.edge
349            .get()
350            .ok_or(PipelineError::NoEdge)?
351            .write(data)
352            .await
353    }
354
355    fn set_edge(&self, edge: Edge<Out>, _: private::Token) -> Result<(), PipelineError> {
356        self.edge
357            .set(edge)
358            .map_err(|_| PipelineError::EdgeAlreadySet)?;
359
360        Ok(())
361    }
362}
363
364#[async_trait]
365impl<In: PipelineIO, Out: PipelineIO> Sink<In> for PipelineNode<In, Out> {
366    async fn on_data(&self, data: In, _: private::Token) -> Result<(), Error> {
367        self.on_next((self.map_fn)(data)?, private::Token).await
368    }
369}
370
371#[cfg(test)]
372mod tests {
373
374    use super::*;
375    use crate::pipeline::*;
376
377    #[tokio::test]
378    async fn test_pipeline_source_no_edge() {
379        let source = ServiceFrontend::<SingleIn<()>, ManyOut<()>>::new();
380        let stream = source.generate(().into()).await;
381        assert!(stream.is_err());
382    }
383}