Skip to main content

dynamo_runtime/pipeline/
network.rs

1// SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Network layer for distributed communication
5//!
6//! Provides request distribution across multiple transport protocols:
7//! - HTTP/2 for standard deployments
8//! - TCP with length-prefixed protocol for high-performance scenarios
9//! - NATS for legacy/messaging-based deployments
10
11pub mod codec;
12pub mod egress;
13pub mod ingress;
14pub mod manager;
15pub mod quic_response;
16pub mod tcp;
17
18use crate::SystemHealth;
19use crate::error::{DynamoError, ErrorType};
20use crate::traits::DistributedRuntimeProvider;
21use std::sync::{Arc, OnceLock};
22
23use anyhow::Result;
24use async_trait::async_trait;
25use bytes::Bytes;
26use codec::{TwoPartCodec, TwoPartMessage, TwoPartMessageType};
27use derive_builder::Builder;
28use futures::StreamExt;
29// io::Cursor, TryStreamExt
30use super::{AsyncEngine, AsyncEngineContext, AsyncEngineContextProvider, ResponseStream};
31use serde::{Deserialize, Serialize, de::DeserializeOwned};
32
33use super::{
34    AsyncTransportEngine, Context, Data, Error, ManyIn, ManyOut, PipelineError, PipelineIO,
35    SegmentSource, ServiceBackend, ServiceEngine, SingleIn, Source, context,
36};
37use crate::metrics::MetricsHierarchy;
38use crate::metrics::prometheus_names::work_handler;
39use crate::protocols::maybe_error::MaybeError;
40use crate::telemetry::LifecycleOperationRole;
41use ingress::push_handler::WorkHandlerMetrics;
42use prometheus::{CounterVec, Histogram, IntCounter, IntCounterVec, IntGauge};
43
44/// Shared default maximum TCP message size across request-plane components.
45pub(crate) const DEFAULT_TCP_MAX_MESSAGE_SIZE: usize = 32 * 1024 * 1024;
46
47/// Prefix a request-plane server writes on the request connection to reject a request it cannot
48/// serve. The client matches on it to classify the reply as a rejection rather than the success
49/// ACK; anything it does not recognise is read as the ACK and it waits for a response stream.
50/// Both ends must use this constant.
51pub(crate) const ACK_UNAVAILABLE_PREFIX: &str = "Server unavailable:";
52
53static REQUEST_PLANE_PAYLOAD_CODEC: OnceLock<RequestPlanePayloadCodec> = OnceLock::new();
54static RESPONSE_PLANE_MODE: OnceLock<ResponsePlaneMode> = OnceLock::new();
55
56/// Process-wide response transport. Frontends and workers must use the same mode.
57#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
58pub enum ResponsePlaneMode {
59    #[default]
60    Tcp,
61    Quic,
62}
63
64impl ResponsePlaneMode {
65    pub fn configured() -> Result<Self> {
66        if let Some(mode) = RESPONSE_PLANE_MODE.get() {
67            return Ok(*mode);
68        }
69        let value =
70            std::env::var(crate::config::environment_names::response_plane::DYN_RESPONSE_PLANE)
71                .ok();
72        let mode = Self::from_config_value(value.as_deref())?;
73        Ok(*RESPONSE_PLANE_MODE.get_or_init(|| mode))
74    }
75
76    fn from_config_value(value: Option<&str>) -> Result<Self> {
77        match value {
78            None | Some("tcp") => Ok(Self::Tcp),
79            Some("quic") => Ok(Self::Quic),
80            Some(other) => anyhow::bail!(
81                "invalid {} value '{other}'; expected 'tcp' or 'quic'",
82                crate::config::environment_names::response_plane::DYN_RESPONSE_PLANE
83            ),
84        }
85    }
86
87    pub fn from_transport_name(transport: &str) -> Result<Self> {
88        match transport {
89            "tcp_server" => Ok(Self::Tcp),
90            quic_response::TRANSPORT_NAME => Ok(Self::Quic),
91            other => anyhow::bail!("unsupported response transport '{other}'"),
92        }
93    }
94
95    pub const fn name(self) -> &'static str {
96        match self {
97            Self::Tcp => "tcp",
98            Self::Quic => "quic",
99        }
100    }
101}
102
103crate::env_config! {
104    /// Read the configured TCP max message size once and share it across client,
105    /// server, and zero-copy decoder code paths.
106    pub(crate) fn get_tcp_max_message_size() -> usize =
107        crate::config::environment_names::request_plane::DYN_TCP_MAX_MESSAGE_SIZE,
108        default = DEFAULT_TCP_MAX_MESSAGE_SIZE;
109}
110
111#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
112#[serde(rename_all = "snake_case")]
113pub enum RequestPlanePayloadCodec {
114    /// The serde default deliberately remains JSON for wire compatibility with
115    /// control messages produced before the payload codec field existed.
116    #[default]
117    Json,
118    Msgpack,
119}
120
121impl RequestPlanePayloadCodec {
122    pub fn configured() -> Self {
123        *REQUEST_PLANE_PAYLOAD_CODEC.get_or_init(Self::from_env)
124    }
125
126    fn from_env() -> Self {
127        let value =
128            std::env::var(crate::config::environment_names::request_plane::DYN_REQUEST_PLANE_CODEC)
129                .ok();
130        Self::from_config_value(value.as_deref())
131    }
132
133    fn from_config_value(value: Option<&str>) -> Self {
134        match value {
135            None | Some("") | Some("msgpack") => Self::Msgpack,
136            Some("json") => Self::Json,
137            Some(other) => {
138                tracing::warn!(
139                    env_var =
140                        crate::config::environment_names::request_plane::DYN_REQUEST_PLANE_CODEC,
141                    value = other,
142                    "invalid request plane payload codec, defaulting to msgpack"
143                );
144                Self::Msgpack
145            }
146        }
147    }
148
149    pub fn name(&self) -> &'static str {
150        match self {
151            Self::Json => "json",
152            Self::Msgpack => "msgpack",
153        }
154    }
155
156    pub fn encode<T: Serialize>(&self, value: &T) -> Result<Vec<u8>> {
157        match self {
158            Self::Json => Ok(serde_json::to_vec(value)?),
159            Self::Msgpack => Ok(rmp_serde::to_vec_named(value)?),
160        }
161    }
162
163    /// Encode into a caller-owned writer, so a hot path can reuse one buffer
164    /// across frames instead of allocating per frame.
165    ///
166    /// Emits the same bytes as [`Self::encode`], which
167    /// `encode_into_matches_encode_byte_for_byte` pins for both codecs.
168    pub fn encode_into<T, W>(&self, value: &T, writer: &mut W) -> Result<()>
169    where
170        T: Serialize + ?Sized,
171        W: std::io::Write,
172    {
173        match self {
174            Self::Json => Ok(serde_json::to_writer(writer, value)?),
175            Self::Msgpack => Ok(rmp_serde::encode::write_named(writer, value)?),
176        }
177    }
178
179    pub fn decode<T: DeserializeOwned>(&self, bytes: &[u8]) -> Result<T> {
180        match self {
181            Self::Json => Ok(serde_json::from_slice(bytes)?),
182            Self::Msgpack => Ok(rmp_serde::from_slice(bytes)?),
183        }
184    }
185}
186
187/// Starting capacity for a buffer that one response frame is encoded into.
188///
189/// Matches `serde_json::to_vec`'s own default so msgpack — which otherwise
190/// starts at zero — stops regrowing its buffer on every streamed frame.
191pub const RESPONSE_ENCODE_CAPACITY_HINT: usize = 128;
192
193pub trait Codable: PipelineIO + Serialize + for<'de> Deserialize<'de> {}
194impl<T: PipelineIO + Serialize + for<'de> Deserialize<'de>> Codable for T {}
195
196/// `WorkQueueConsumer` is a generic interface for a work queue that can be used to send and receive
197#[async_trait]
198pub trait WorkQueueConsumer {
199    async fn dequeue(&self) -> Result<Bytes, String>;
200}
201
202#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
203#[serde(rename_all = "snake_case")]
204pub enum StreamType {
205    Request,
206    Response,
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize)]
210#[serde(rename_all = "snake_case")]
211pub(crate) enum RequestType {
212    SingleIn,
213    ManyIn,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize)]
217#[serde(rename_all = "snake_case")]
218pub(crate) enum ResponseType {
219    SingleOut,
220    ManyOut,
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize)]
224pub(crate) struct RequestControlMessage {
225    pub(crate) id: String,
226    pub(crate) request_type: RequestType,
227    pub(crate) response_type: ResponseType,
228    #[serde(default)]
229    pub(crate) payload_codec: RequestPlanePayloadCodec,
230    pub(crate) connection_info: ConnectionInfo,
231    #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
232    pub(crate) metadata: std::collections::BTreeMap<String, String>,
233    /// Wall-clock send timestamp (nanos since UNIX epoch) for transport latency breakdown.
234    /// Uses `SystemTime` so accuracy depends on NTP sync between frontend and backend hosts.
235    /// Reliable for single-machine profiling; treat cross-host values as approximate.
236    #[serde(default, skip_serializing_if = "Option::is_none")]
237    pub(crate) frontend_send_ts_ns: Option<u64>,
238    /// For bidirectional dispatch (`request_type == ManyIn`): connection info the
239    /// worker dials back to in order to receive subsequent request frames. `None`
240    /// for the unary path, which is the wire-compatible default.
241    #[serde(default, skip_serializing_if = "Option::is_none")]
242    pub(crate) request_stream_connection_info: Option<ConnectionInfo>,
243}
244
245#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
246#[serde(rename_all = "snake_case")]
247pub enum ControlMessage {
248    Stop,
249    Kill,
250    Sentinel,
251}
252
253/// This is the first message in a `ResponseStream`. This is not a message that gets process
254/// by the general pipeline, but is a control message that is awaited before the
255/// [`AsyncEngine::generate`] method is allowed to return.
256///
257/// If an error is present, the [`AsyncEngine::generate`] method will return the error instead
258/// of returning the `ResponseStream`.
259#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
260pub struct ResponseStreamPrologue {
261    error: Option<String>,
262
263    /// The same failure as `error`, but keeping the worker's
264    /// [`crate::error::ErrorType`] instead of only its display text, so the
265    /// frontend can classify a
266    /// pre-stream failure (for example, a backend refusing a request it can
267    /// never serve) rather than guessing from a message.
268    ///
269    /// `Option` plus `#[serde(default)]` is a compatibility requirement, not a
270    /// convenience: worker and frontend are deployed independently, so during a
271    /// rolling upgrade an old worker sends a prologue without this field and a
272    /// new worker sends one an old frontend does not know. A required field
273    /// would break the handshake in both directions; an absent field or unrecognized legacy
274    /// `error_type` decodes to `None` and the frontend falls back to the untyped behavior.
275    /// A payload with the semantic `class` field uses `DynamoError`'s fail-closed policy.
276    #[serde(
277        default,
278        deserialize_with = "deserialize_typed_error",
279        skip_serializing_if = "Option::is_none"
280    )]
281    typed_error: Option<DynamoError>,
282}
283
284fn deserialize_typed_error<'de, D>(deserializer: D) -> Result<Option<DynamoError>, D::Error>
285where
286    D: serde::Deserializer<'de>,
287{
288    let typed_error = Option::<serde_json::Value>::deserialize(deserializer)?;
289    Ok(typed_error.and_then(|typed_error| {
290        // The legacy enum decoder rejected future variants. ErrorClass is tolerant, so
291        // compare the raw value with its canonical round trip to preserve that fallback.
292        if typed_error.get("class").is_none()
293            && let Some(wire_error_type) = typed_error.get("error_type")
294        {
295            let round_trip = serde_json::from_value::<ErrorType>(wire_error_type.clone())
296                .ok()
297                .and_then(|error_type| serde_json::to_value(error_type).ok());
298            if round_trip.as_ref() != Some(wire_error_type) {
299                return None;
300            }
301        }
302        serde_json::from_value(typed_error).ok()
303    }))
304}
305
306/// A pre-stream failure as it reaches the requesting side of the transport.
307///
308/// `message` is the display text the prologue has always carried.
309/// `typed_error` is the worker's own [`DynamoError`] when the worker was new
310/// enough to send one.
311#[derive(Debug, Clone, PartialEq, Eq)]
312pub struct StreamPrologueError {
313    pub message: String,
314    pub typed_error: Option<DynamoError>,
315}
316
317impl StreamPrologueError {
318    /// A failure detected by the transport itself, with no worker error behind it.
319    pub fn from_message(message: impl Into<String>) -> Self {
320        Self {
321            message: message.into(),
322            typed_error: None,
323        }
324    }
325
326    /// A worker failure, keeping the display text the prologue has always
327    /// carried alongside the worker's typed error.
328    pub fn new(message: impl Into<String>, typed_error: DynamoError) -> Self {
329        Self {
330            message: message.into(),
331            typed_error: Some(typed_error),
332        }
333    }
334}
335
336impl std::fmt::Display for StreamPrologueError {
337    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
338        f.write_str(&self.message)
339    }
340}
341
342/// Derefs to the message so code written against the previous `String` error
343/// keeps working: `err.contains(..)`, `err.len()`, `&err[..]` all still resolve.
344/// Only an explicit `String` type annotation or signature needs updating.
345impl std::ops::Deref for StreamPrologueError {
346    type Target = str;
347
348    fn deref(&self) -> &Self::Target {
349        &self.message
350    }
351}
352
353impl AsRef<str> for StreamPrologueError {
354    fn as_ref(&self) -> &str {
355        &self.message
356    }
357}
358
359impl From<String> for StreamPrologueError {
360    fn from(message: String) -> Self {
361        Self::from_message(message)
362    }
363}
364
365impl From<&str> for StreamPrologueError {
366    fn from(message: &str) -> Self {
367        Self::from_message(message)
368    }
369}
370
371/// Resolves once the worker's response stream is established, or with a
372/// [`StreamPrologueError`] carrying both the failure's display text and, when
373/// the worker sent one, its [`crate::error::ErrorType`], so the requesting side
374/// can classify a pre-stream failure without parsing the message.
375pub type StreamProvider<T> = tokio::sync::oneshot::Receiver<Result<T, StreamPrologueError>>;
376
377/// Owning `Drop` here (rather than on `RegisteredStream`) lets `into_parts()`
378/// move the public fields out by plain destructure.
379struct Cleanup(Option<Box<dyn FnOnce() + Send + 'static>>);
380
381impl Drop for Cleanup {
382    fn drop(&mut self) {
383        if let Some(f) = self.0.take() {
384            f();
385        }
386    }
387}
388
389/// Awaitable handle for a stream sender or receiver. Dropping the handle without
390/// a successful `wait()` or a call to `into_parts()` runs the optional cleanup
391/// closure, removing the registration from the stream server's maps.
392pub struct RegisteredStream<T> {
393    pub connection_info: ConnectionInfo,
394    pub stream_provider: StreamProvider<T>,
395    registration_id: Option<uuid::Uuid>,
396    cleanup: Cleanup,
397}
398
399impl<T> std::fmt::Debug for RegisteredStream<T> {
400    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
401        f.debug_struct("RegisteredStream")
402            .field("connection_info", &self.connection_info)
403            .finish_non_exhaustive()
404    }
405}
406
407impl<T> RegisteredStream<T> {
408    pub(crate) fn new(connection_info: ConnectionInfo, stream_provider: StreamProvider<T>) -> Self {
409        Self {
410            connection_info,
411            stream_provider,
412            registration_id: None,
413            cleanup: Cleanup(None),
414        }
415    }
416
417    pub(crate) fn with_registration_id(mut self, registration_id: uuid::Uuid) -> Self {
418        self.registration_id = Some(registration_id);
419        self
420    }
421
422    pub(crate) fn registration_id(&self) -> Option<uuid::Uuid> {
423        self.registration_id
424    }
425
426    pub(crate) fn with_cleanup<F>(mut self, cleanup: F) -> Self
427    where
428        F: FnOnce() + Send + 'static,
429    {
430        self.cleanup.0 = Some(Box::new(cleanup));
431        self
432    }
433
434    /// Consume the registration, disarming the RAII cleanup. Caller takes
435    /// responsibility for cleanup if the stream provider is never awaited.
436    pub fn into_parts(self) -> (ConnectionInfo, StreamProvider<T>) {
437        let Self {
438            connection_info,
439            stream_provider,
440            registration_id: _,
441            mut cleanup,
442        } = self;
443        cleanup.0.take();
444        (connection_info, stream_provider)
445    }
446
447    /// Await the stream provider, keeping registration cleanup armed until success.
448    ///
449    /// An error or a dropped future removes the registration from the transport.
450    /// Once the stream is established, cleanup is disarmed and the transport owns
451    /// the stream's remaining lifecycle.
452    pub async fn wait(
453        mut self,
454    ) -> Result<Result<T, StreamPrologueError>, tokio::sync::oneshot::error::RecvError> {
455        let result = (&mut self.stream_provider).await;
456        if matches!(result, Ok(Ok(_))) {
457            self.cleanup.0.take();
458        }
459        result
460    }
461}
462
463/// After registering a stream, the [`PendingConnections`] object is returned to the caller. This
464/// object can be used to await the connection to be established.
465pub struct PendingConnections {
466    pub send_stream: Option<RegisteredStream<StreamSender>>,
467    pub recv_stream: Option<RegisteredStream<StreamReceiver>>,
468}
469
470impl PendingConnections {
471    pub fn into_parts(
472        self,
473    ) -> (
474        Option<RegisteredStream<StreamSender>>,
475        Option<RegisteredStream<StreamReceiver>>,
476    ) {
477        (self.send_stream, self.recv_stream)
478    }
479}
480
481/// A [`ResponseService`] implements a services in which a context a specific subject with will
482/// be associated with a stream of responses.
483#[async_trait::async_trait]
484pub trait ResponseService {
485    async fn register(&self, options: StreamOptions) -> PendingConnections;
486}
487
488#[cfg(test)]
489mod registered_stream_tests {
490    use super::*;
491    use std::sync::atomic::{AtomicBool, Ordering};
492
493    fn dummy_conn_info() -> ConnectionInfo {
494        ConnectionInfo {
495            transport: "test".to_string(),
496            info: "{}".to_string(),
497        }
498    }
499
500    /// Drop without `into_parts()` must run the cleanup closure.
501    #[test]
502    fn drop_runs_cleanup() {
503        let flag = Arc::new(AtomicBool::new(false));
504        let flag_clone = flag.clone();
505
506        let (_tx, rx) = tokio::sync::oneshot::channel::<Result<(), StreamPrologueError>>();
507        let stream = RegisteredStream::new(dummy_conn_info(), rx).with_cleanup(move || {
508            flag_clone.store(true, Ordering::SeqCst);
509        });
510
511        drop(stream);
512        assert!(
513            flag.load(Ordering::SeqCst),
514            "cleanup must fire when RegisteredStream is dropped"
515        );
516    }
517
518    /// `into_parts()` must disarm the cleanup. After the call, dropping the
519    /// returned halves must NOT trigger the closure -- the caller has taken
520    /// ownership of cleanup responsibility.
521    #[test]
522    fn into_parts_disarms_cleanup() {
523        let flag = Arc::new(AtomicBool::new(false));
524        let flag_clone = flag.clone();
525
526        let (_tx, rx) = tokio::sync::oneshot::channel::<Result<(), StreamPrologueError>>();
527        let stream = RegisteredStream::new(dummy_conn_info(), rx).with_cleanup(move || {
528            flag_clone.store(true, Ordering::SeqCst);
529        });
530
531        let (conn, provider) = stream.into_parts();
532        drop(conn);
533        drop(provider);
534
535        assert!(
536            !flag.load(Ordering::SeqCst),
537            "into_parts() must disarm the cleanup closure"
538        );
539    }
540
541    #[tokio::test]
542    async fn successful_wait_disarms_cleanup() {
543        let flag = Arc::new(AtomicBool::new(false));
544        let flag_clone = flag.clone();
545        let (tx, rx) = tokio::sync::oneshot::channel();
546        let stream = RegisteredStream::new(dummy_conn_info(), rx).with_cleanup(move || {
547            flag_clone.store(true, Ordering::SeqCst);
548        });
549
550        tx.send(Ok(42)).unwrap();
551        assert_eq!(stream.wait().await.unwrap().unwrap(), 42);
552        assert!(
553            !flag.load(Ordering::SeqCst),
554            "successful wait must disarm cleanup"
555        );
556    }
557
558    #[tokio::test]
559    async fn failed_prologue_wait_runs_cleanup() {
560        let flag = Arc::new(AtomicBool::new(false));
561        let flag_clone = flag.clone();
562        let (tx, rx) = tokio::sync::oneshot::channel::<Result<(), StreamPrologueError>>();
563        let stream = RegisteredStream::new(dummy_conn_info(), rx).with_cleanup(move || {
564            flag_clone.store(true, Ordering::SeqCst);
565        });
566        let error = StreamPrologueError::from_message("worker rejected request");
567
568        tx.send(Err(error.clone())).unwrap();
569        assert_eq!(stream.wait().await.unwrap().unwrap_err(), error);
570        assert!(
571            flag.load(Ordering::SeqCst),
572            "failed prologue must run cleanup"
573        );
574    }
575
576    #[tokio::test]
577    async fn closed_provider_wait_runs_cleanup() {
578        let flag = Arc::new(AtomicBool::new(false));
579        let flag_clone = flag.clone();
580        let (tx, rx) = tokio::sync::oneshot::channel::<Result<(), StreamPrologueError>>();
581        let stream = RegisteredStream::new(dummy_conn_info(), rx).with_cleanup(move || {
582            flag_clone.store(true, Ordering::SeqCst);
583        });
584
585        drop(tx);
586        assert!(stream.wait().await.is_err());
587        assert!(
588            flag.load(Ordering::SeqCst),
589            "closed provider must run cleanup"
590        );
591    }
592
593    /// `RegisteredStream` with no cleanup configured must drop cleanly.
594    #[test]
595    fn drop_without_cleanup_is_a_noop() {
596        let (_tx, rx) = tokio::sync::oneshot::channel::<Result<(), StreamPrologueError>>();
597        let stream: RegisteredStream<()> = RegisteredStream::new(dummy_conn_info(), rx);
598        drop(stream); // must not panic; nothing observable to assert beyond that
599    }
600}
601
602// #[derive(Debug, Clone, Serialize, Deserialize)]
603// struct Handshake {
604//     request_id: String,
605//     worker_id: Option<String>,
606//     error: Option<String>,
607// }
608
609// impl Handshake {
610//     pub fn validate(&self) -> Result<(), String> {
611//         if let Some(e) = &self.error {
612//             return Err(e.clone());
613//         }
614//         Ok(())
615//     }
616// }
617
618// this probably needs to be come a ResponseStreamSender
619// since the prologue in this scenario sender telling the receiver
620// that all is good and it's ready to send
621//
622// in the RequestStreamSender, the prologue would be coming from the
623// receiver, so the sender would have to await the prologue which if
624// was not an error, would indicate the RequestStreamReceiver is read
625// to receive data.
626pub struct StreamSender {
627    tx: tokio::sync::mpsc::Sender<TwoPartMessage>,
628    prologue: Option<ResponseStreamPrologue>,
629}
630
631impl StreamSender {
632    pub async fn send(&self, data: Bytes) -> Result<()> {
633        Ok(self.tx.send(TwoPartMessage::from_data(data)).await?)
634    }
635
636    pub async fn send_control(&self, control: ControlMessage) -> Result<()> {
637        let bytes = serde_json::to_vec(&control)?;
638        Ok(self
639            .tx
640            .send(TwoPartMessage::from_header(bytes.into()))
641            .await?)
642    }
643
644    /// Send the prologue, reporting an untyped failure.
645    ///
646    /// A caller holding the worker's typed error uses
647    /// [`Self::send_prologue_typed`] instead, which keeps that type on the wire.
648    pub async fn send_prologue(&mut self, error: Option<String>) -> Result<(), String> {
649        self.send_prologue_typed(error.map(StreamPrologueError::from_message))
650            .await
651    }
652
653    pub async fn send_prologue_typed(
654        &mut self,
655        error: Option<StreamPrologueError>,
656    ) -> Result<(), String> {
657        // leaving the original logic in place for now
658        if let Some(_prologue) = self.prologue.take() {
659            let (error, typed_error) = match error {
660                Some(StreamPrologueError {
661                    message,
662                    typed_error,
663                }) => (Some(message), typed_error),
664                None => (None, None),
665            };
666            let prologue = ResponseStreamPrologue { error, typed_error };
667            let header_bytes: Bytes = match serde_json::to_vec(&prologue) {
668                Ok(b) => b.into(),
669                Err(err) => {
670                    tracing::error!(%err, "send_prologue: ResponseStreamPrologue did not serialize to a JSON array");
671                    return Err("Invalid prologue".to_string());
672                }
673            };
674            self.tx
675                .send(TwoPartMessage::from_header(header_bytes))
676                .await
677                .map_err(|e| e.to_string())?;
678        } else {
679            panic!("Prologue already sent; or not set; logic error");
680        }
681        Ok(())
682    }
683}
684
685pub struct StreamReceiver {
686    rx: tokio::sync::mpsc::Receiver<Bytes>,
687}
688
689/// Connection Info is encoded as JSON and then again serialized has part of the Transport
690/// Layer. The double serialization is not performance critical as it is only done once per
691/// connection. The primary reason storing the ConnecitonInfo has a JSON string is for type
692/// erasure. The Transport Layer will check the [`ConnectionInfo::transport`] type and then
693/// route it to the appropriate instance of the Transport, which will then deserialize the
694/// [`ConnectionInfo::info`] field to its internal connection info object.
695///
696/// Optionally, this object could become strongly typed for which all possible combinations
697/// of transport and connection info would need to be enumerated.
698#[derive(Debug, Clone, Serialize, Deserialize)]
699pub struct ConnectionInfo {
700    pub transport: String,
701    pub info: String,
702}
703
704/// Default number of frames buffered between the data-plane socket task and the
705/// engine consumer/producer for a single stream. Preserves the historically
706/// hard-coded mpsc channel capacity used by the TCP transport.
707pub const DEFAULT_SEND_BUFFER_COUNT: usize = 64;
708
709/// When registering a new TransportStream on the server, the caller specifies if the
710/// stream is a sender, receiver or both.
711///
712/// Senders and Receivers are with share a Context, but result in separate tcp socket
713/// connections to the server. Internally, we may use bcast channels to coordinate the
714/// internal control messages between the sender and receiver socket connections.
715#[derive(Clone, Builder)]
716pub struct StreamOptions {
717    /// Context
718    pub context: Arc<dyn AsyncEngineContext>,
719
720    /// Register with the server that this connection will have a server-side Sender
721    /// that can be picked up by the Request/Forward pipeline. The downstream side
722    /// dials in via [`crate::pipeline::network::tcp::client::TcpClient::create_request_stream`]
723    /// to receive the frames the server pushes.
724    pub enable_request_stream: bool,
725
726    /// Register with the server that this connection will have a server-side Receiver
727    /// that can be picked up by the Response/Reverse pipeline
728    pub enable_response_stream: bool,
729
730    /// Preserve guarded dispatch setup until the worker sends its prologue.
731    #[builder(default)]
732    pub defer_cancellation_until_prologue: bool,
733
734    /// The number of frames buffered between the data-plane socket task and the
735    /// engine consumer/producer before backpressure kicks in. Drives the mpsc
736    /// channel capacity for the per-stream buffer in the TCP transport.
737    #[builder(default = "DEFAULT_SEND_BUFFER_COUNT")]
738    pub send_buffer_count: usize,
739
740    /// The number of messages to buffer before blocking
741    #[builder(default = "8")]
742    pub recv_buffer_count: usize,
743}
744
745impl StreamOptions {
746    pub fn builder() -> StreamOptionsBuilder {
747        StreamOptionsBuilder::default()
748    }
749}
750
751pub struct Egress<Req: PipelineIO, Resp: PipelineIO> {
752    transport_engine: Arc<dyn AsyncTransportEngine<Req, Resp>>,
753}
754
755#[cfg(test)]
756mod tests {
757    use super::{
758        DEFAULT_SEND_BUFFER_COUNT, Ingress, IngressResponseEncoder, ManyOut, NetworkStreamWrapper,
759        RequestControlMessage, RequestPlanePayloadCodec, RequestType, ResponsePlaneMode,
760        ResponseStreamPrologue, ResponseType, SerdeIngressPayloadAdapter, SingleIn, StreamOptions,
761        StreamPrologueError,
762    };
763    use crate::engine::AsyncEngineContextProvider;
764    use crate::error::{BackendError, DynamoError, ErrorType};
765    use crate::pipeline::Context;
766    use crate::protocols::annotated::Annotated;
767    use crate::telemetry::LifecycleOperationRole;
768    use serde::{Deserialize, Serialize};
769    use std::sync::{Arc, OnceLock};
770
771    #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
772    struct TestPayload {
773        id: u64,
774        text: String,
775        tokens: Vec<u32>,
776    }
777
778    #[test]
779    fn prologue_without_typed_error_field_still_decodes() {
780        let legacy = br#"{"error":"Generate Error: something went wrong"}"#;
781        let prologue: ResponseStreamPrologue =
782            serde_json::from_slice(legacy).expect("a prologue without the typed field must decode");
783
784        assert_eq!(
785            prologue.error.as_deref(),
786            Some("Generate Error: something went wrong")
787        );
788        assert!(
789            prologue.typed_error.is_none(),
790            "an absent typed error must decode to None, not fail"
791        );
792    }
793
794    /// Pins the `String` compat shim on the widened `StreamProvider` error:
795    /// `str` methods and `String` conversion must keep resolving without a
796    /// field access, or the source break gets wider than intended.
797    #[test]
798    fn stream_prologue_error_substitutes_for_the_previous_string() {
799        let err = StreamPrologueError::from_message("malformed prologue: bad header");
800
801        assert!(err.contains("malformed prologue"));
802        assert!(err.starts_with("malformed"));
803        assert_eq!(err.len(), "malformed prologue: bad header".len());
804        assert_eq!(&err[..9], "malformed");
805        assert_eq!(err.as_ref() as &str, "malformed prologue: bad header");
806
807        let converted: StreamPrologueError = "from a &str".into();
808        assert_eq!(converted.message, "from a &str");
809        assert!(converted.typed_error.is_none());
810
811        let converted: StreamPrologueError = String::from("from a String").into();
812        assert_eq!(converted.message, "from a String");
813    }
814
815    #[test]
816    fn prologue_round_trips_the_typed_error() {
817        let prologue = ResponseStreamPrologue {
818            error: Some("Generate Error: unsupported input".to_string()),
819            typed_error: Some(
820                DynamoError::builder()
821                    .error_type(ErrorType::Backend(BackendError::InvalidArgument))
822                    .message("unsupported input")
823                    .build(),
824            ),
825        };
826
827        let encoded = serde_json::to_vec(&prologue).expect("prologue should serialize");
828        let decoded: ResponseStreamPrologue =
829            serde_json::from_slice(&encoded).expect("prologue should deserialize");
830
831        assert_eq!(decoded, prologue);
832        assert_eq!(
833            decoded.typed_error.map(|e| e.error_type()),
834            Some(ErrorType::Backend(BackendError::InvalidArgument))
835        );
836    }
837
838    #[test]
839    fn prologue_accepts_a_known_legacy_typed_error() {
840        let legacy = br#"{
841            "error":"Generate Error: unsupported input",
842            "typed_error":{"error_type":"InvalidArgument","message":"unsupported input"}
843        }"#;
844        let prologue: ResponseStreamPrologue =
845            serde_json::from_slice(legacy).expect("a known legacy typed error must decode");
846        let error = prologue
847            .typed_error
848            .expect("a known legacy typed error must remain typed");
849
850        assert_eq!(error.error_type(), ErrorType::InvalidArgument);
851        assert_eq!(error.class(), ErrorType::InvalidRequest);
852        assert_eq!(error.reason().as_str(), "request.invalid_argument");
853    }
854
855    #[test]
856    fn prologue_ignores_a_future_legacy_typed_error() {
857        let future = br#"{
858            "error":"Generate Error: future failure",
859            "typed_error":{"error_type":"VariantFromTheFuture","message":"future failure"}
860        }"#;
861        let prologue: ResponseStreamPrologue = serde_json::from_slice(future)
862            .expect("a future legacy typed error must not reject the prologue");
863
864        assert!(prologue.typed_error.is_none());
865    }
866
867    #[test]
868    fn prologue_fails_closed_for_a_future_semantic_class() {
869        let future = br#"{
870            "error":"Generate Error: future failure",
871            "typed_error":{
872                "class":"FutureErrorClass",
873                "reason":"runtime.internal",
874                "public":{"type":"message","message":"must not escape"}
875            }
876        }"#;
877        let prologue: ResponseStreamPrologue = serde_json::from_slice(future)
878            .expect("a future semantic class must not reject the prologue");
879        let error = prologue
880            .typed_error
881            .expect("semantic errors use the DynamoError fail-closed identity");
882
883        assert_eq!(error.class(), ErrorType::Internal);
884        assert_eq!(error.reason().as_str(), "runtime.invalid_error");
885        assert!(error.public_details().is_none());
886    }
887
888    #[test]
889    fn response_plane_mode_parses_supported_values() {
890        assert_eq!(
891            ResponsePlaneMode::from_config_value(None).unwrap(),
892            ResponsePlaneMode::Tcp
893        );
894        assert_eq!(
895            ResponsePlaneMode::from_config_value(Some("tcp")).unwrap(),
896            ResponsePlaneMode::Tcp
897        );
898        assert_eq!(
899            ResponsePlaneMode::from_config_value(Some("quic")).unwrap(),
900            ResponsePlaneMode::Quic
901        );
902        assert!(ResponsePlaneMode::from_config_value(Some("")).is_err());
903        assert!(ResponsePlaneMode::from_config_value(Some("invalid")).is_err());
904    }
905
906    #[test]
907    fn encode_into_matches_encode_byte_for_byte() {
908        let payload = NetworkStreamWrapper {
909            data: Some(TestPayload {
910                id: 7,
911                text: "hello".to_string(),
912                tokens: vec![1, 200, 70_000],
913            }),
914            complete_final: false,
915        };
916
917        for codec in [
918            RequestPlanePayloadCodec::Json,
919            RequestPlanePayloadCodec::Msgpack,
920        ] {
921            let expected = codec.encode(&payload).expect("encode");
922            let mut actual = Vec::new();
923            codec
924                .encode_into(&payload, &mut actual)
925                .expect("encode_into");
926            assert_eq!(expected, actual, "codec={}", codec.name());
927        }
928    }
929
930    #[test]
931    fn stream_options_send_buffer_count_defaults_to_64() {
932        let context = Context::new(());
933        let options = StreamOptions::builder()
934            .context(context.context())
935            .enable_request_stream(true)
936            .enable_response_stream(true)
937            .build()
938            .expect("stream options should build");
939
940        assert_eq!(DEFAULT_SEND_BUFFER_COUNT, 64);
941        assert_eq!(options.send_buffer_count, DEFAULT_SEND_BUFFER_COUNT);
942    }
943
944    #[test]
945    fn stream_options_send_buffer_count_overrides_default() {
946        let context = Context::new(());
947        let options = StreamOptions::builder()
948            .context(context.context())
949            .enable_request_stream(true)
950            .enable_response_stream(true)
951            .send_buffer_count(128)
952            .build()
953            .expect("stream options should build");
954
955        assert_eq!(options.send_buffer_count, 128);
956    }
957
958    #[test]
959    fn ingress_observes_lifecycle_role_set_after_binding() {
960        let ingress = Ingress::<SingleIn<String>, ManyOut<String>>::new();
961        let endpoint_role = Arc::new(OnceLock::new());
962
963        ingress
964            .lifecycle_operation_role
965            .set(endpoint_role.clone())
966            .expect("new ingress lifecycle role source must be empty");
967        assert_eq!(ingress.registered_lifecycle_role(), None);
968
969        endpoint_role
970            .set(LifecycleOperationRole::Prefill)
971            .expect("new endpoint lifecycle role cell must be empty");
972        assert_eq!(
973            ingress.registered_lifecycle_role(),
974            Some(LifecycleOperationRole::Prefill)
975        );
976    }
977
978    #[test]
979    fn legacy_frontend_control_message_defaults_payload_codec_to_json() {
980        let json = r#"{
981            "id": "request-123",
982            "request_type": "single_in",
983            "response_type": "many_out",
984            "connection_info": {
985                "transport": "tcp",
986                "info": "{}"
987            }
988        }"#;
989
990        let message: RequestControlMessage =
991            serde_json::from_str(json).expect("control message should deserialize");
992
993        assert_eq!(message.id, "request-123");
994        assert!(matches!(message.request_type, RequestType::SingleIn));
995        assert!(matches!(message.response_type, ResponseType::ManyOut));
996        assert_eq!(message.payload_codec, RequestPlanePayloadCodec::Json);
997        assert_eq!(message.connection_info.transport, "tcp");
998        assert_eq!(message.connection_info.info, "{}");
999        assert!(message.metadata.is_empty());
1000        assert!(message.frontend_send_ts_ns.is_none());
1001
1002        let payload = br#"{"id":7,"text":"legacy","tokens":[1,2]}"#;
1003        let decoded: TestPayload = message
1004            .payload_codec
1005            .decode(payload)
1006            .expect("worker should decode the legacy frontend's JSON payload");
1007        assert_eq!(
1008            decoded,
1009            TestPayload {
1010                id: 7,
1011                text: "legacy".to_string(),
1012                tokens: vec![1, 2],
1013            }
1014        );
1015    }
1016
1017    #[test]
1018    fn request_control_message_decodes_msgpack_payload_codec() {
1019        let json = r#"{
1020            "id": "request-123",
1021            "request_type": "single_in",
1022            "response_type": "many_out",
1023            "payload_codec": "msgpack",
1024            "connection_info": {
1025                "transport": "tcp",
1026                "info": "{}"
1027            }
1028        }"#;
1029
1030        let message: RequestControlMessage =
1031            serde_json::from_str(json).expect("control message should deserialize");
1032
1033        assert_eq!(message.payload_codec, RequestPlanePayloadCodec::Msgpack);
1034    }
1035
1036    #[test]
1037    fn request_plane_payload_codec_configuration_defaults_to_msgpack() {
1038        assert_eq!(
1039            RequestPlanePayloadCodec::from_config_value(None),
1040            RequestPlanePayloadCodec::Msgpack
1041        );
1042        assert_eq!(
1043            RequestPlanePayloadCodec::from_config_value(Some("")),
1044            RequestPlanePayloadCodec::Msgpack
1045        );
1046        assert_eq!(
1047            RequestPlanePayloadCodec::from_config_value(Some("invalid")),
1048            RequestPlanePayloadCodec::Msgpack
1049        );
1050    }
1051
1052    #[test]
1053    fn request_plane_payload_codec_configuration_honors_explicit_overrides() {
1054        assert_eq!(
1055            RequestPlanePayloadCodec::from_config_value(Some("json")),
1056            RequestPlanePayloadCodec::Json
1057        );
1058        assert_eq!(
1059            RequestPlanePayloadCodec::from_config_value(Some("msgpack")),
1060            RequestPlanePayloadCodec::Msgpack
1061        );
1062    }
1063
1064    #[test]
1065    fn request_plane_payload_codec_round_trips_response_wrapper_json_and_msgpack() {
1066        let wrapper = NetworkStreamWrapper {
1067            data: Some(TestPayload {
1068                id: 42,
1069                text: "line\nquote\"slash\\unicode 中".to_string(),
1070                tokens: vec![1, 2, 3, 65535],
1071            }),
1072            complete_final: false,
1073        };
1074
1075        for codec in [
1076            RequestPlanePayloadCodec::Json,
1077            RequestPlanePayloadCodec::Msgpack,
1078        ] {
1079            let encoded = codec.encode(&wrapper).expect("wrapper should encode");
1080            let decoded: NetworkStreamWrapper<TestPayload> =
1081                codec.decode(&encoded).expect("wrapper should decode");
1082            assert_eq!(decoded, wrapper);
1083        }
1084    }
1085
1086    /// `encode_into` must stay byte-compatible with `encode` for both codecs.
1087    #[tokio::test]
1088    async fn serde_ingress_encoder_matches_encode_byte_for_byte() {
1089        let data = Annotated::from_data(serde_json::json!({
1090            "token_ids": [128, 9001],
1091            "index": 3,
1092        }));
1093        let error = Annotated::<serde_json::Value>::from_error("engine failed");
1094
1095        for codec in [
1096            RequestPlanePayloadCodec::Json,
1097            RequestPlanePayloadCodec::Msgpack,
1098        ] {
1099            for (case, response, complete_final, expect_error) in [
1100                ("data frame", Some(data.clone()), false, false),
1101                ("error frame", Some(error.clone()), false, true),
1102                ("complete final", None, true, false),
1103            ] {
1104                let expected = codec
1105                    .encode(&NetworkStreamWrapper {
1106                        data: response.clone(),
1107                        complete_final,
1108                    })
1109                    .expect("reference encode");
1110
1111                let frame = SerdeIngressPayloadAdapter
1112                    .encode_response(codec, response, complete_final)
1113                    .await
1114                    .expect("adapter encode");
1115
1116                assert_eq!(
1117                    frame.bytes.as_ref(),
1118                    expected.as_slice(),
1119                    "codec={} case={case}",
1120                    codec.name()
1121                );
1122                assert_eq!(
1123                    frame.is_error,
1124                    expect_error,
1125                    "codec={} case={case}",
1126                    codec.name()
1127                );
1128                assert!(!frame.stop_stream, "codec={} case={case}", codec.name());
1129            }
1130        }
1131    }
1132}
1133
1134#[async_trait]
1135impl<T: Data, U: Data> AsyncEngine<SingleIn<T>, ManyOut<U>, Error>
1136    for Egress<SingleIn<T>, ManyOut<U>>
1137where
1138    T: Data + Serialize,
1139    U: for<'de> Deserialize<'de> + Data,
1140{
1141    async fn generate(&self, request: SingleIn<T>) -> Result<ManyOut<U>, Error> {
1142        self.transport_engine.generate(request).await
1143    }
1144}
1145
1146/// Result of encoding one response item for the request plane.
1147pub struct EncodedResponseFrame {
1148    pub bytes: Bytes,
1149    pub is_error: bool,
1150    /// Stop consuming the engine stream after publishing this frame. The
1151    /// normal complete-final frame is still sent.
1152    pub stop_stream: bool,
1153}
1154
1155/// Converts request-plane bytes into the item consumed by an ingress engine.
1156pub trait IngressRequestDecoder<T>: Send + Sync + 'static
1157where
1158    T: Data,
1159{
1160    fn decode_request(
1161        &self,
1162        payload_codec: RequestPlanePayloadCodec,
1163        bytes: Bytes,
1164    ) -> impl std::future::Future<Output = std::result::Result<T, PipelineError>> + Send;
1165}
1166
1167/// Converts an ingress engine response into its complete on-wire frame.
1168pub trait IngressResponseEncoder<U>: Send + Sync + 'static
1169where
1170    U: Data,
1171{
1172    fn encode_response(
1173        &self,
1174        payload_codec: RequestPlanePayloadCodec,
1175        response: Option<U>,
1176        complete_final: bool,
1177    ) -> impl std::future::Future<Output = std::result::Result<EncodedResponseFrame, PipelineError>> + Send;
1178}
1179
1180/// Complete request/response payload adapter for an ingress engine.
1181pub trait IngressPayloadAdapter<T, U>:
1182    IngressRequestDecoder<T> + IngressResponseEncoder<U>
1183where
1184    T: Data,
1185    U: Data,
1186{
1187}
1188
1189impl<T, U, Adapter> IngressPayloadAdapter<T, U> for Adapter
1190where
1191    T: Data,
1192    U: Data,
1193    Adapter: IngressRequestDecoder<T> + IngressResponseEncoder<U>,
1194{
1195}
1196
1197/// Default adapter for ordinary Rust request and response types.
1198#[derive(Debug, Default)]
1199pub struct SerdeIngressPayloadAdapter;
1200
1201impl<T> IngressRequestDecoder<T> for SerdeIngressPayloadAdapter
1202where
1203    T: Data + DeserializeOwned,
1204{
1205    #[inline]
1206    fn decode_request(
1207        &self,
1208        payload_codec: RequestPlanePayloadCodec,
1209        bytes: Bytes,
1210    ) -> impl std::future::Future<Output = std::result::Result<T, PipelineError>> + Send {
1211        let decoded = payload_codec.decode(&bytes).map_err(|err| {
1212            PipelineError::DeserializationError(format!(
1213                "Failed deserializing {} request payload: {}",
1214                payload_codec.name(),
1215                err
1216            ))
1217        });
1218        std::future::ready(decoded)
1219    }
1220}
1221
1222impl<U> IngressResponseEncoder<U> for SerdeIngressPayloadAdapter
1223where
1224    U: Data + Serialize + MaybeError,
1225{
1226    #[inline]
1227    fn encode_response(
1228        &self,
1229        payload_codec: RequestPlanePayloadCodec,
1230        response: Option<U>,
1231        complete_final: bool,
1232    ) -> impl std::future::Future<Output = std::result::Result<EncodedResponseFrame, PipelineError>> + Send
1233    {
1234        let is_error = response
1235            .as_ref()
1236            .is_some_and(|response| response.err().is_some());
1237        let wrapper = NetworkStreamWrapper {
1238            data: response,
1239            complete_final,
1240        };
1241        let mut bytes = Vec::with_capacity(RESPONSE_ENCODE_CAPACITY_HINT);
1242        let encoded = payload_codec
1243            .encode_into(&wrapper, &mut bytes)
1244            .map(|()| bytes)
1245            .map_err(|err| {
1246                PipelineError::SerializationError(format!(
1247                    "Failed serializing {} request-plane response: {}",
1248                    payload_codec.name(),
1249                    err
1250                ))
1251            });
1252        std::future::ready(encoded.map(|bytes| EncodedResponseFrame {
1253            bytes: bytes.into(),
1254            is_error,
1255            stop_stream: false,
1256        }))
1257    }
1258}
1259
1260pub struct Ingress<Req: PipelineIO, Resp: PipelineIO, Adapter = SerdeIngressPayloadAdapter> {
1261    segment: OnceLock<Arc<SegmentSource<Req, Resp>>>,
1262    metrics: OnceLock<Arc<WorkHandlerMetrics>>,
1263    /// Endpoint-specific notifier for health check timer resets
1264    endpoint_health_check_notifier: OnceLock<Arc<tokio::sync::Notify>>,
1265    /// Resolved response transport of the bound endpoint's runtime.
1266    response_plane: OnceLock<ResponsePlaneMode>,
1267    quic_response_client_pool: OnceLock<Arc<quic_response::QuicResponseClientPool>>,
1268    payload_adapter: Arc<Adapter>,
1269    lifecycle_operation_role: OnceLock<Arc<OnceLock<LifecycleOperationRole>>>,
1270}
1271
1272impl<Req: PipelineIO + Sync, Resp: PipelineIO> Ingress<Req, Resp> {
1273    pub fn new() -> Arc<Self> {
1274        Ingress::new_with_adapter(SerdeIngressPayloadAdapter)
1275    }
1276
1277    pub fn link(segment: Arc<SegmentSource<Req, Resp>>) -> Result<Arc<Self>> {
1278        let ingress = Ingress::new();
1279        ingress.attach(segment)?;
1280        Ok(ingress)
1281    }
1282
1283    pub fn for_pipeline(segment: Arc<SegmentSource<Req, Resp>>) -> Result<Arc<Self>> {
1284        let ingress = Ingress::new();
1285        ingress.attach(segment)?;
1286        Ok(ingress)
1287    }
1288
1289    pub fn for_engine(engine: ServiceEngine<Req, Resp>) -> Result<Arc<Self>> {
1290        Self::for_engine_with_adapter(engine, SerdeIngressPayloadAdapter)
1291    }
1292
1293    /// Build a worker ingress with the role fixed by its startup configuration.
1294    pub fn for_engine_with_lifecycle_role(
1295        engine: ServiceEngine<Req, Resp>,
1296        role: LifecycleOperationRole,
1297    ) -> Result<Arc<Self>> {
1298        Self::for_engine_with_adapter_and_lifecycle_role(engine, SerdeIngressPayloadAdapter, role)
1299    }
1300}
1301
1302impl<Req, Resp, Adapter> Ingress<Req, Resp, Adapter>
1303where
1304    Req: PipelineIO + Sync,
1305    Resp: PipelineIO,
1306    Adapter: Send + Sync + 'static,
1307{
1308    pub fn new_with_adapter(payload_adapter: Adapter) -> Arc<Self> {
1309        Arc::new(Self {
1310            segment: OnceLock::new(),
1311            metrics: OnceLock::new(),
1312            endpoint_health_check_notifier: OnceLock::new(),
1313            response_plane: OnceLock::new(),
1314            quic_response_client_pool: OnceLock::new(),
1315            payload_adapter: Arc::new(payload_adapter),
1316            lifecycle_operation_role: OnceLock::new(),
1317        })
1318    }
1319
1320    fn new_with_adapter_and_lifecycle_role(
1321        payload_adapter: Adapter,
1322        lifecycle_operation_role: LifecycleOperationRole,
1323    ) -> Arc<Self> {
1324        let ingress = Self::new_with_adapter(payload_adapter);
1325        let role = Arc::new(OnceLock::new());
1326        role.set(lifecycle_operation_role)
1327            .expect("new lifecycle role cell must be empty");
1328        ingress
1329            .lifecycle_operation_role
1330            .set(role)
1331            .expect("new ingress lifecycle role source must be empty");
1332        ingress
1333    }
1334
1335    pub fn attach(&self, segment: Arc<SegmentSource<Req, Resp>>) -> Result<()> {
1336        self.segment
1337            .set(segment)
1338            .map_err(|_| anyhow::anyhow!("Segment already set"))
1339    }
1340
1341    pub(crate) fn set_quic_response_client_pool(
1342        &self,
1343        pool: Arc<quic_response::QuicResponseClientPool>,
1344    ) -> Result<()> {
1345        self.quic_response_client_pool
1346            .set(pool)
1347            .map_err(|_| anyhow::anyhow!("QUIC response client pool already set"))
1348    }
1349
1350    pub(crate) fn quic_response_client_pool(
1351        &self,
1352    ) -> Result<Arc<quic_response::QuicResponseClientPool>, PipelineError> {
1353        if let Some(pool) = self.quic_response_client_pool.get() {
1354            return Ok(pool.clone());
1355        }
1356        let pool = quic_response::process_client_pool_from_env()?;
1357        Ok(self.quic_response_client_pool.get_or_init(|| pool).clone())
1358    }
1359
1360    pub fn add_metrics(
1361        &self,
1362        endpoint: &crate::component::Endpoint,
1363        metrics_labels: Option<&[(&str, &str)]>,
1364    ) -> Result<()> {
1365        let metrics = WorkHandlerMetrics::from_endpoint(endpoint, metrics_labels)
1366            .map_err(|e| anyhow::anyhow!("Failed to create work handler metrics: {}", e))?;
1367
1368        // Register global transport breakdown metrics (idempotent)
1369        crate::metrics::work_handler_perf::ensure_work_handler_perf_metrics_registered(
1370            endpoint.get_metrics_registry(),
1371        );
1372
1373        // Register worker-pool saturation metrics (idempotent). These are
1374        // process-global and shared across all endpoints attached to the
1375        // same shared TCP server.
1376        crate::metrics::work_handler_pool::ensure_work_handler_pool_metrics_registered(
1377            endpoint.get_metrics_registry(),
1378        );
1379
1380        self.metrics
1381            .set(Arc::new(metrics))
1382            .map_err(|_| anyhow::anyhow!("Metrics already set"))
1383    }
1384
1385    pub fn for_engine_with_adapter(
1386        engine: ServiceEngine<Req, Resp>,
1387        payload_adapter: Adapter,
1388    ) -> Result<Arc<Self>> {
1389        let frontend = SegmentSource::<Req, Resp>::new();
1390        let backend = ServiceBackend::from_engine(engine);
1391        let pipeline = frontend.link(backend)?.link_terminal(frontend)?;
1392        let ingress = Ingress::new_with_adapter(payload_adapter);
1393        ingress.attach(pipeline)?;
1394        Ok(ingress)
1395    }
1396
1397    pub fn for_engine_with_adapter_and_lifecycle_role(
1398        engine: ServiceEngine<Req, Resp>,
1399        payload_adapter: Adapter,
1400        role: LifecycleOperationRole,
1401    ) -> Result<Arc<Self>> {
1402        let frontend = SegmentSource::<Req, Resp>::new();
1403        let backend = ServiceBackend::from_engine(engine);
1404
1405        let pipeline = frontend.link(backend)?.link_terminal(frontend)?;
1406
1407        let ingress = Ingress::new_with_adapter_and_lifecycle_role(payload_adapter, role);
1408        ingress.attach(pipeline)?;
1409
1410        Ok(ingress)
1411    }
1412
1413    /// Helper method to access metrics if available
1414    fn metrics(&self) -> Option<&Arc<WorkHandlerMetrics>> {
1415        self.metrics.get()
1416    }
1417
1418    fn registered_lifecycle_role(&self) -> Option<LifecycleOperationRole> {
1419        self.lifecycle_operation_role
1420            .get()
1421            .and_then(|role| role.get())
1422            .copied()
1423    }
1424
1425    fn bind_endpoint_config(&self, endpoint: &crate::component::Endpoint) {
1426        let _ = self.response_plane.set(endpoint.drt().response_plane());
1427        // Keep an explicitly constructed ingress role; otherwise observe model
1428        // registration even when it happens after the endpoint starts serving.
1429        let _ = self
1430            .lifecycle_operation_role
1431            .set(endpoint.lifecycle_operation_role());
1432    }
1433}
1434
1435#[async_trait]
1436pub trait PushWorkHandler: Send + Sync {
1437    /// Bind endpoint identity independently of metrics registration.
1438    fn bind_endpoint(&self, _endpoint: &crate::component::Endpoint) {}
1439
1440    async fn handle_payload(
1441        &self,
1442        payload: Bytes,
1443        request_id: Option<String>,
1444    ) -> Result<(), PipelineError>;
1445
1446    /// Add metrics to the handler
1447    fn add_metrics(
1448        &self,
1449        endpoint: &crate::component::Endpoint,
1450        metrics_labels: Option<&[(&str, &str)]>,
1451    ) -> Result<()>;
1452
1453    /// Set the endpoint-specific notifier for health check timer resets
1454    fn set_endpoint_health_check_notifier(
1455        &self,
1456        _notifier: Arc<tokio::sync::Notify>,
1457    ) -> Result<()> {
1458        // Default implementation for backwards compatibility
1459        Ok(())
1460    }
1461}
1462
1463/*
1464/// `NetworkStreamWrapper` is a simple wrapper used to detect proper stream termination
1465/// in network communication between ingress and egress components.
1466///
1467/// **Purpose**: This wrapper solves the problem of detecting whether a stream ended
1468/// gracefully or was cut off prematurely (e.g., due to network issues).
1469///
1470/// **Design Rationale**:
1471/// - Cannot use `Annotated` directly because the generic type `U` varies:
1472///   - Sometimes `U = Annotated<...>`
1473///   - Sometimes `U = LLMEngineOutput<...>`
1474/// - Using `Annotated` would require double-wrapping like `Annotated<Annotated<...>>`
1475/// - A simple wrapper is cleaner and more straightforward
1476///
1477/// **Stream Flow**:
1478/// ```
1479/// At AsyncEngine:
1480///   response 1 -> response 2 -> response 3 -> <end>
1481///
1482/// Between ingress/egress:
1483///   response 1 <end=false> -> response 2 <end=false> -> response 3 <end=false> -> (null) <end=true>
1484///
1485/// At client:
1486///   response 1 -> response 2 -> response 3 -> <end>
1487/// ```
1488///
1489/// **Error Handling**:
1490/// If the stream is cut off before proper termination, the egress is responsible for
1491/// injecting an error response to communicate the incomplete stream to the client:
1492/// ```
1493/// At AsyncEngine:
1494///   response 1 -> ... <without end flag>
1495///
1496/// At egress:
1497///   response 1 <end=false> -> <stream ended without end flag -> convert to error>
1498///
1499/// At client:
1500///   response 1 -> error response
1501/// ```
1502///
1503/// The detection must be done at egress level because premature stream termination
1504/// can be due to network issues that only the egress component can detect.
1505*/
1506/// TODO: Detect end-of-stream using Server-Sent Events (SSE). This will be removed.
1507#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
1508pub struct NetworkStreamWrapper<U> {
1509    #[serde(skip_serializing_if = "Option::is_none")]
1510    pub data: Option<U>,
1511    pub complete_final: bool,
1512}