1use super::*;
5
6use crate::engine::AsyncEngineContext;
7use crate::error::DynamoError;
8use crate::metrics::prometheus_names::work_handler;
9use crate::metrics::work_handler_perf::{
10 WORK_HANDLER_NETWORK_TRANSIT_SECONDS, WORK_HANDLER_TIME_TO_FIRST_RESPONSE_SECONDS,
11};
12use crate::pipeline::network::StreamPrologueError;
13use crate::pipeline::{ManyIn, RequestStream};
14use crate::telemetry::{LifecycleStage, LifecycleTrace};
15use futures::StreamExt;
16use prometheus::{Histogram, IntCounter, IntCounterVec, IntGauge};
17use serde::Deserialize;
18use std::sync::Arc;
19use std::time::Instant;
20use tracing::Instrument;
21use tracing::info_span;
22
23#[derive(Clone, Debug)]
25pub struct WorkHandlerMetrics {
26 pub request_counter: IntCounter,
27 pub request_duration: Histogram,
28 pub inflight_requests: IntGauge,
29 pub request_bytes: IntCounter,
30 pub response_bytes: IntCounter,
31 pub error_counter: IntCounterVec,
32 pub cancellation_total: IntCounter,
33}
34
35impl WorkHandlerMetrics {
36 pub fn new(
37 request_counter: IntCounter,
38 request_duration: Histogram,
39 inflight_requests: IntGauge,
40 request_bytes: IntCounter,
41 response_bytes: IntCounter,
42 error_counter: IntCounterVec,
43 cancellation_total: IntCounter,
44 ) -> Self {
45 Self {
46 request_counter,
47 request_duration,
48 inflight_requests,
49 request_bytes,
50 response_bytes,
51 error_counter,
52 cancellation_total,
53 }
54 }
55
56 pub fn from_endpoint(
58 endpoint: &crate::component::Endpoint,
59 metrics_labels: Option<&[(&str, &str)]>,
60 ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
61 let metrics_labels = metrics_labels.unwrap_or(&[]);
62 let metrics = endpoint.metrics();
63 let request_counter = metrics.create_intcounter(
64 work_handler::REQUESTS_TOTAL,
65 "Total number of requests processed by work handler",
66 metrics_labels,
67 )?;
68
69 let request_duration_buckets = vec![
73 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 20.0, 30.0, 60.0, 120.0,
74 300.0, 600.0,
75 ];
76 let request_duration = metrics.create_histogram(
77 work_handler::REQUEST_DURATION_SECONDS,
78 "Time spent processing requests by work handler",
79 metrics_labels,
80 Some(request_duration_buckets),
81 )?;
82
83 let inflight_requests = metrics.create_intgauge(
84 work_handler::INFLIGHT_REQUESTS,
85 "Number of requests currently being processed by work handler",
86 metrics_labels,
87 )?;
88
89 let request_bytes = metrics.create_intcounter(
90 work_handler::REQUEST_BYTES_TOTAL,
91 "Total number of bytes received in requests by work handler",
92 metrics_labels,
93 )?;
94
95 let response_bytes = metrics.create_intcounter(
96 work_handler::RESPONSE_BYTES_TOTAL,
97 "Total number of bytes sent in responses by work handler",
98 metrics_labels,
99 )?;
100
101 let error_counter = metrics.create_intcountervec(
102 work_handler::ERRORS_TOTAL,
103 "Total number of errors in work handler processing",
104 &[work_handler::ERROR_TYPE_LABEL],
105 metrics_labels,
106 )?;
107
108 let cancellation_total = metrics.create_intcounter(
109 work_handler::CANCELLATION_TOTAL,
110 "Total number of requests cancelled by work handler",
111 metrics_labels,
112 )?;
113
114 Ok(Self::new(
115 request_counter,
116 request_duration,
117 inflight_requests,
118 request_bytes,
119 response_bytes,
120 error_counter,
121 cancellation_total,
122 ))
123 }
124}
125
126struct RequestMetricsGuard {
129 inflight_requests: prometheus::IntGauge,
130 request_duration: prometheus::Histogram,
131 start_time: Instant,
132 request_id: Option<String>,
133}
134
135impl Drop for RequestMetricsGuard {
136 fn drop(&mut self) {
137 self.inflight_requests.dec();
138 self.request_duration
139 .observe(self.start_time.elapsed().as_secs_f64());
140 if let Some(request_id) = &self.request_id {
141 tracing::info!(request_id = %request_id, "request completed");
142 }
143 }
144}
145
146trait ResponsePublisher {
147 async fn send(&self, payload: Bytes) -> anyhow::Result<()>;
148 async fn send_prologue(&mut self, error: Option<String>) -> anyhow::Result<()>;
149
150 async fn send_prologue_typed(
156 &mut self,
157 error: Option<StreamPrologueError>,
158 ) -> anyhow::Result<()> {
159 self.send_prologue(error.map(|error| error.message)).await
160 }
161 async fn finish(&mut self) -> anyhow::Result<()>;
162 async fn abort(&mut self) -> anyhow::Result<()>;
163
164 fn reset_on_stop(&self) -> bool {
165 false
166 }
167
168 fn strict_prologue(&self) -> bool {
169 false
170 }
171}
172
173impl ResponsePublisher for quic_response::QuicResponseSender {
174 async fn send(&self, payload: Bytes) -> anyhow::Result<()> {
175 quic_response::QuicResponseSender::send(self, payload).await
176 }
177
178 async fn send_prologue(&mut self, error: Option<String>) -> anyhow::Result<()> {
179 quic_response::QuicResponseSender::send_prologue(self, error)
180 .await
181 .map_err(anyhow::Error::msg)
182 }
183
184 async fn send_prologue_typed(
185 &mut self,
186 error: Option<StreamPrologueError>,
187 ) -> anyhow::Result<()> {
188 quic_response::QuicResponseSender::send_prologue_typed(self, error)
189 .await
190 .map_err(anyhow::Error::msg)
191 }
192
193 async fn finish(&mut self) -> anyhow::Result<()> {
194 quic_response::QuicResponseSender::finish(self).await
195 }
196
197 async fn abort(&mut self) -> anyhow::Result<()> {
198 quic_response::QuicResponseSender::abort(self).await
199 }
200
201 fn reset_on_stop(&self) -> bool {
202 true
203 }
204
205 fn strict_prologue(&self) -> bool {
206 true
207 }
208}
209
210impl ResponsePublisher for StreamSender {
211 async fn send(&self, payload: Bytes) -> anyhow::Result<()> {
212 StreamSender::send(self, payload).await
213 }
214
215 async fn send_prologue(&mut self, error: Option<String>) -> anyhow::Result<()> {
216 StreamSender::send_prologue(self, error)
217 .await
218 .map_err(anyhow::Error::msg)
219 }
220
221 async fn send_prologue_typed(
222 &mut self,
223 error: Option<StreamPrologueError>,
224 ) -> anyhow::Result<()> {
225 StreamSender::send_prologue_typed(self, error)
226 .await
227 .map_err(anyhow::Error::msg)
228 }
229
230 async fn finish(&mut self) -> anyhow::Result<()> {
231 Ok(())
232 }
233
234 async fn abort(&mut self) -> anyhow::Result<()> {
235 Ok(())
236 }
237}
238
239impl<Req, Resp, Adapter> Ingress<Req, Resp, Adapter>
240where
241 Req: PipelineIO + Sync,
242 Resp: PipelineIO,
243 Adapter: Send + Sync + 'static,
244{
245 async fn pump_response_stream<U>(
252 &self,
253 mut stream: ManyOut<U>,
254 publisher: &impl ResponsePublisher,
255 payload_codec: RequestPlanePayloadCodec,
256 ) where
257 U: Data + std::fmt::Debug,
258 Adapter: IngressResponseEncoder<U>,
259 {
260 let context = stream.context();
261
262 let mut send_complete_final = true;
264 let mut saw_error_response = false;
265 while let Some(resp) = stream.next().await {
266 tracing::trace!("Sending response: {:?}", resp);
267 let encoded = match self
268 .payload_adapter
269 .encode_response(payload_codec, Some(resp), false)
270 .await
271 {
272 Ok(encoded) => encoded,
273 Err(err) => {
274 tracing::error!(%err, "failed to encode request-plane response");
275 saw_error_response = true;
276 send_complete_final = false;
277 if let Some(m) = self.metrics() {
278 m.error_counter
279 .with_label_values(&[work_handler::error_types::SERIALIZATION])
280 .inc();
281 }
282 break;
283 }
284 };
285 let is_error = encoded.is_error;
286 saw_error_response |= is_error;
287 let resp_bytes = encoded.bytes;
288 if let Some(m) = self.metrics() {
289 m.response_bytes.inc_by(resp_bytes.len() as u64);
290 }
291 if (publisher.send(resp_bytes).await).is_err() {
292 send_complete_final = false;
293 if context.is_stopped() {
294 tracing::warn!("Failed to publish response for stream {}", context.id());
303 } else {
304 tracing::error!("Failed to publish response for stream {}", context.id());
306 context.stop_generating();
307 }
308 if let Some(m) = self.metrics() {
311 m.error_counter
312 .with_label_values(&[work_handler::error_types::PUBLISH_RESPONSE])
313 .inc();
314 }
315 break;
316 } else if !is_error {
317 if let Some(notifier) = self.endpoint_health_check_notifier.get() {
320 notifier.notify_one();
321 }
322 }
323 if encoded.stop_stream {
324 break;
329 }
330 }
331 if publisher.reset_on_stop() && context.is_stopped() && !context.is_killed() {
335 send_complete_final = false;
336 }
337 if send_complete_final {
338 let encoded = match self
339 .payload_adapter
340 .encode_response(payload_codec, None, true)
341 .await
342 {
343 Ok(encoded) => encoded,
344 Err(err) => {
345 tracing::error!(%err, "failed to encode request-plane final response");
346 if let Some(m) = self.metrics() {
347 m.error_counter
348 .with_label_values(&[work_handler::error_types::PUBLISH_FINAL])
349 .inc();
350 }
351 return;
352 }
353 };
354 let resp_bytes = encoded.bytes;
355 if let Some(m) = self.metrics() {
356 m.response_bytes.inc_by(resp_bytes.len() as u64);
357 }
358 if (publisher.send(resp_bytes).await).is_err() {
359 if context.is_stopped() && !context.is_killed() {
370 tracing::debug!(
374 "Failed to publish complete final for stream {}; client already torn down",
375 context.id()
376 );
377 } else {
378 tracing::error!(
382 "Failed to publish complete final for stream {}",
383 context.id()
384 );
385 if let Some(m) = self.metrics() {
386 m.error_counter
387 .with_label_values(&[work_handler::error_types::PUBLISH_FINAL])
388 .inc();
389 }
390 }
391 }
392 if let (false, Some(notifier)) = (
394 saw_error_response,
395 self.endpoint_health_check_notifier.get(),
396 ) {
397 notifier.notify_one();
398 }
399 }
400 }
401
402 fn decode_control_message(
413 &self,
414 payload: Bytes,
415 ) -> Result<(RequestControlMessage, Option<Bytes>), PipelineError> {
416 let msg = TwoPartCodec::default()
417 .decode_message(payload)?
418 .into_message_type();
419
420 let (header, data) = match msg {
421 TwoPartMessageType::HeaderAndData(header, data) => (header, Some(data)),
422 TwoPartMessageType::HeaderOnly(header) => (header, None),
423 _ => {
424 if let Some(m) = self.metrics() {
425 m.error_counter
426 .with_label_values(&[work_handler::error_types::INVALID_MESSAGE])
427 .inc();
428 }
429 return Err(PipelineError::Generic(String::from(
430 "Unexpected message from work queue; expected a header-only or header-and-data TwoPartMessage",
431 )));
432 }
433 };
434
435 let control_msg: RequestControlMessage =
436 serde_json::from_slice(&header).map_err(|err| {
437 if let Some(m) = self.metrics() {
438 m.error_counter
439 .with_label_values(&[work_handler::error_types::DESERIALIZATION])
440 .inc();
441 }
442 let json_str = String::from_utf8_lossy(&header);
443 PipelineError::DeserializationError(format!(
444 "Failed deserializing to RequestControlMessage. err={err}, json_str={json_str}, header_len={}",
445 header.len(),
446 ))
447 })?;
448
449 Ok((control_msg, data))
450 }
451}
452struct ParsedRequest<Req> {
457 request: Req,
458 response_connection_info: ConnectionInfo,
459 frontend_send_ts_ns: Option<u64>,
460 payload_codec: RequestPlanePayloadCodec,
461}
462
463#[derive(Clone, Copy)]
464struct ResponsePlaneModes {
465 configured: ResponsePlaneMode,
466 advertised: ResponsePlaneMode,
467}
468
469#[async_trait]
476trait IngressDispatch: Send + Sync {
477 type Request: PipelineIO;
478
479 async fn parse_and_build_request(
480 &self,
481 control_msg: RequestControlMessage,
482 data: Option<Bytes>,
483 ) -> Result<ParsedRequest<Self::Request>, PipelineError>;
484}
485
486#[async_trait]
487impl<T, U, Adapter> IngressDispatch for Ingress<SingleIn<T>, ManyOut<U>, Adapter>
488where
489 T: Data + for<'de> Deserialize<'de> + std::fmt::Debug,
490 U: Data + std::fmt::Debug,
491 Adapter: IngressRequestDecoder<T> + Send + Sync + 'static,
492{
493 type Request = SingleIn<T>;
494
495 async fn parse_and_build_request(
496 &self,
497 control_msg: RequestControlMessage,
498 data: Option<Bytes>,
499 ) -> Result<ParsedRequest<SingleIn<T>>, PipelineError> {
500 let data = data.ok_or_else(|| {
503 if let Some(m) = self.metrics() {
504 m.error_counter
505 .with_label_values(&[work_handler::error_types::INVALID_MESSAGE])
506 .inc();
507 }
508 PipelineError::Generic(String::from(
509 "unary engine received a header-only envelope; expected a request payload",
510 ))
511 })?;
512 let payload_codec = control_msg.payload_codec;
513 let request_t: T = self
514 .payload_adapter
515 .decode_request(payload_codec, data)
516 .await
517 .inspect_err(|_| {
518 if let Some(m) = self.metrics() {
519 m.error_counter
520 .with_label_values(&[work_handler::error_types::DESERIALIZATION])
521 .inc();
522 }
523 })?;
524
525 tracing::trace!(
526 request_id = %control_msg.id,
527 metadata_entries = control_msg.metadata.len(),
528 "received control message"
529 );
530 tracing::trace!("received request: {:?}", request_t);
531
532 let request: context::Context<T> =
533 Context::with_id_and_metadata(request_t, control_msg.id, control_msg.metadata);
534
535 Ok(ParsedRequest {
536 request,
537 response_connection_info: control_msg.connection_info,
538 frontend_send_ts_ns: control_msg.frontend_send_ts_ns,
539 payload_codec,
540 })
541 }
542}
543
544#[async_trait]
545impl<T, U, Adapter> IngressDispatch for Ingress<ManyIn<T>, ManyOut<U>, Adapter>
546where
547 T: Data + for<'de> Deserialize<'de> + std::fmt::Debug,
548 U: Data + std::fmt::Debug,
549 Adapter: IngressRequestDecoder<T> + Send + Sync + 'static,
550{
551 type Request = ManyIn<T>;
552
553 async fn parse_and_build_request(
554 &self,
555 control_msg: RequestControlMessage,
556 data: Option<Bytes>,
557 ) -> Result<ParsedRequest<ManyIn<T>>, PipelineError> {
558 if data.is_some() {
563 if let Some(m) = self.metrics() {
564 m.error_counter
565 .with_label_values(&[work_handler::error_types::INVALID_MESSAGE])
566 .inc();
567 }
568 return Err(PipelineError::Generic(String::from(
569 "bidirectional engine received a non-header-only envelope",
570 )));
571 }
572
573 if !matches!(control_msg.request_type, RequestType::ManyIn) {
574 if let Some(m) = self.metrics() {
575 m.error_counter
576 .with_label_values(&[work_handler::error_types::INVALID_MESSAGE])
577 .inc();
578 }
579 return Err(PipelineError::Generic(String::from(
580 "bidirectional engine received a non-ManyIn request envelope",
581 )));
582 }
583
584 let req_stream_conn_info = control_msg
585 .request_stream_connection_info
586 .clone()
587 .ok_or_else(|| {
588 PipelineError::Generic(String::from(
589 "bidirectional control message missing request_stream_connection_info",
590 ))
591 })?;
592
593 let request_context: context::Context<()> = context::Context::with_id_and_metadata(
594 (),
595 control_msg.id.clone(),
596 control_msg.metadata.clone(),
597 );
598 let payload_codec = control_msg.payload_codec;
599 let context_arc: Arc<dyn AsyncEngineContext> = request_context.context();
600
601 let request_stream_recv = tcp::client::TcpClient::create_request_stream(
607 context_arc.clone(),
608 req_stream_conn_info,
609 None,
610 )
611 .await
612 .map_err(|e| {
613 if let Some(m) = self.metrics() {
614 m.error_counter
615 .with_label_values(&[work_handler::error_types::RESPONSE_STREAM])
616 .inc();
617 }
618 PipelineError::Generic(format!("Failed to create request stream: {e}"))
619 })?;
620
621 let (frame_tx, frame_rx) = tokio::sync::mpsc::channel::<T>(8);
626 let forwarder_ctx = context_arc.clone();
627 let payload_adapter = self.payload_adapter.clone();
628 tokio::spawn(async move {
629 let mut rx = request_stream_recv.rx;
630 while let Some(bytes) = rx.recv().await {
631 if forwarder_ctx.is_killed() || forwarder_ctx.is_stopped() {
636 break;
637 }
638 match payload_adapter.decode_request(payload_codec, bytes).await {
639 Ok(item) => {
640 if frame_tx.send(item).await.is_err() {
641 tracing::debug!(
642 "engine consumer dropped; bidirectional input forwarder exiting"
643 );
644 break;
645 }
646 }
647 Err(e) => {
648 tracing::error!(
649 error = %e,
650 codec = payload_codec.name(),
651 "failed to deserialize bidirectional request frame; killing context"
652 );
653 forwarder_ctx.kill();
654 break;
655 }
656 }
657 }
658 });
659
660 let input_stream: crate::engine::DataStream<T> =
661 Box::pin(tokio_stream::wrappers::ReceiverStream::new(frame_rx));
662 let request: ManyIn<T> = request_context.map(|_| RequestStream::new(input_stream));
663
664 Ok(ParsedRequest {
665 request,
666 response_connection_info: control_msg.connection_info,
667 frontend_send_ts_ns: control_msg.frontend_send_ts_ns,
668 payload_codec,
669 })
670 }
671}
672
673impl<Req, U, Adapter> Ingress<Req, ManyOut<U>, Adapter>
674where
675 Req: PipelineIO + Sync,
676 U: Data + std::fmt::Debug,
677 Adapter: IngressResponseEncoder<U> + Send + Sync + 'static,
678{
679 async fn generate_and_publish<P>(
680 &self,
681 request: Req,
682 payload_codec: RequestPlanePayloadCodec,
683 start_time: Instant,
684 response_modes: ResponsePlaneModes,
685 lifecycle: &LifecycleTrace,
686 mut publisher: P,
687 ) -> Result<(), PipelineError>
688 where
689 Self: IngressDispatch<Request = Req>,
690 P: ResponsePublisher,
691 {
692 let ResponsePlaneModes {
693 configured: configured_mode,
694 advertised: advertised_mode,
695 } = response_modes;
696
697 if configured_mode != advertised_mode {
698 let message = format!(
699 "response plane mismatch: frontend requested {}, worker configured {}",
700 advertised_mode.name(),
701 configured_mode.name()
702 );
703 let _ = publisher.send_prologue(Some(message.clone())).await;
704 let _ = publisher.finish().await;
705 return Err(PipelineError::Generic(message));
706 }
707
708 let request_context = request.context();
709 tracing::trace!("calling generate");
710 let worker_operation = lifecycle.start_worker_operation();
711 let stream = async {
712 self.segment
713 .get()
714 .expect("segment not set")
715 .generate(request)
716 .instrument(lifecycle.start(LifecycleStage::RequestDispatch))
717 .await
718 }
719 .instrument(worker_operation.clone())
720 .await
721 .map_err(|error| {
722 if let Some(metrics) = self.metrics() {
723 metrics
724 .error_counter
725 .with_label_values(&[work_handler::error_types::GENERATE])
726 .inc();
727 }
728 PipelineError::GenerateError(error)
729 });
730
731 let stream = match stream {
732 Ok(stream) => {
733 tracing::trace!("Successfully generated response stream; sending prologue");
734 let result = publisher.send_prologue(None).await;
735 if publisher.strict_prologue() {
736 result.map_err(|error| {
737 if let Some(metrics) = self.metrics() {
738 metrics
739 .error_counter
740 .with_label_values(&[work_handler::error_types::RESPONSE_STREAM])
741 .inc();
742 }
743 PipelineError::Generic(format!(
744 "Failed to open {} response stream: {error}",
745 configured_mode.name()
746 ))
747 })?;
748 }
749 WORK_HANDLER_TIME_TO_FIRST_RESPONSE_SECONDS
750 .observe(start_time.elapsed().as_secs_f64());
751 stream
752 }
753 Err(error) => {
754 let error_string = error.to_string();
755
756 #[cfg(debug_assertions)]
757 tracing::debug!(
758 "Failed to generate response stream (with debug backtrace): {:?}",
759 error
760 );
761 #[cfg(not(debug_assertions))]
762 tracing::error!("Failed to generate response stream: {error_string}");
763
764 if publisher.reset_on_stop()
765 && request_context.is_stopped()
766 && !request_context.is_killed()
767 {
768 let _ = publisher.abort().await;
769 } else {
770 let prologue_error = StreamPrologueError::new(
774 error_string,
775 typed_error_from_pipeline_error(&error),
776 );
777 let _ = publisher.send_prologue_typed(Some(prologue_error)).await;
778 }
779 return Err(error);
780 }
781 };
782
783 async {
784 self.pump_response_stream(stream, &publisher, payload_codec)
785 .instrument(lifecycle.start_worker_response_streaming())
786 .await
787 }
788 .instrument(worker_operation)
789 .await;
790 let finish = if publisher.reset_on_stop()
791 && request_context.is_stopped()
792 && !request_context.is_killed()
793 {
794 publisher.abort().await
795 } else {
796 publisher.finish().await
797 };
798 finish.map_err(|error| {
799 PipelineError::Generic(format!(
800 "Failed to finish {} response stream: {error}",
801 configured_mode.name()
802 ))
803 })
804 }
805
806 async fn handle_payload_shared(
814 &self,
815 payload: Bytes,
816 request_id: Option<String>,
817 ) -> Result<(), PipelineError>
818 where
819 Self: IngressDispatch<Request = Req>,
820 {
821 let t2_wallclock_ns = std::time::SystemTime::now()
822 .duration_since(std::time::UNIX_EPOCH)
823 .unwrap_or_default()
824 .as_nanos() as u64;
825 let start_time = std::time::Instant::now();
826 let _inflight_guard = self.metrics().map(|m| {
828 m.request_counter.inc();
829 m.inflight_requests.inc();
830 m.request_bytes.inc_by(payload.len() as u64);
831 if let Some(rid) = &request_id {
832 tracing::info!(request_id = %rid, "request received");
833 }
834 RequestMetricsGuard {
835 inflight_requests: m.inflight_requests.clone(),
836 request_duration: m.request_duration.clone(),
837 start_time,
838 request_id: request_id.clone(),
839 }
840 });
841
842 let (control_msg, data) = self.decode_control_message(payload)?;
843 let lifecycle = match self.registered_lifecycle_role() {
844 Some(role)
845 if control_msg
846 .metadata
847 .get(crate::telemetry::LIFECYCLE_ROOT_METADATA_KEY)
848 .is_some_and(|version| version == "v1") =>
849 {
850 LifecycleTrace::from_request_id_with_role(control_msg.id.clone(), role)
851 }
852 _ => LifecycleTrace::new(false),
853 };
854
855 let worker_admission = lifecycle.start(LifecycleStage::WorkerAdmission);
860 let ParsedRequest {
861 request,
862 response_connection_info,
863 frontend_send_ts_ns,
864 payload_codec,
865 } = self.parse_and_build_request(control_msg, data).await?;
866
867 if let Some(t1_ns) = frontend_send_ts_ns {
869 let transit_ns = t2_wallclock_ns.saturating_sub(t1_ns);
870 WORK_HANDLER_NETWORK_TRANSIT_SECONDS.observe(transit_ns as f64 / 1_000_000_000.0);
871 }
872
873 let advertised_mode =
874 ResponsePlaneMode::from_transport_name(&response_connection_info.transport)
875 .map_err(|error| PipelineError::Generic(error.to_string()))?;
876 let configured_mode = match self.response_plane.get() {
877 Some(mode) => *mode,
878 None => ResponsePlaneMode::configured()
879 .map_err(|error| PipelineError::Generic(error.to_string()))?,
880 };
881 let response_modes = ResponsePlaneModes {
882 configured: configured_mode,
883 advertised: advertised_mode,
884 };
885 let cancellation_counter = self
886 .metrics()
887 .map(|metrics| metrics.cancellation_total.clone());
888
889 match advertised_mode {
890 ResponsePlaneMode::Tcp => {
891 tracing::trace!("creating tcp response stream");
892 let publisher = tcp::client::TcpClient::create_response_stream(
893 request.context(),
894 response_connection_info,
895 cancellation_counter,
896 )
897 .await
898 .map_err(|error| {
899 if let Some(metrics) = self.metrics() {
900 metrics
901 .error_counter
902 .with_label_values(&[work_handler::error_types::RESPONSE_STREAM])
903 .inc();
904 }
905 PipelineError::Generic(format!("Failed to create response stream: {error}"))
906 })?;
907 drop(worker_admission);
908 self.generate_and_publish(
909 request,
910 payload_codec,
911 start_time,
912 response_modes,
913 &lifecycle,
914 publisher,
915 )
916 .await?;
917 }
918 ResponsePlaneMode::Quic => {
919 tracing::trace!("creating QUIC response sender");
920 let response_pool = self.quic_response_client_pool()?;
921 let publisher = response_pool
922 .sender_with_cancellation_metric(
923 request.context(),
924 response_connection_info,
925 cancellation_counter,
926 )
927 .await
928 .map_err(|error| {
929 if let Some(metrics) = self.metrics() {
930 metrics
931 .error_counter
932 .with_label_values(&[work_handler::error_types::RESPONSE_STREAM])
933 .inc();
934 }
935 PipelineError::Generic(format!(
936 "Failed to create QUIC response stream: {error}"
937 ))
938 })?;
939 drop(worker_admission);
940 self.generate_and_publish(
941 request,
942 payload_codec,
943 start_time,
944 response_modes,
945 &lifecycle,
946 publisher,
947 )
948 .await?;
949 }
950 }
951
952 drop(_inflight_guard);
955
956 Ok(())
957 }
958}
959
960#[async_trait]
961impl<T, U, Adapter> PushWorkHandler for Ingress<SingleIn<T>, ManyOut<U>, Adapter>
962where
963 T: Data + for<'de> Deserialize<'de> + std::fmt::Debug,
964 U: Data + std::fmt::Debug,
965 Adapter: IngressPayloadAdapter<T, U> + Send + Sync + 'static,
966{
967 fn bind_endpoint(&self, endpoint: &crate::component::Endpoint) {
968 self.bind_endpoint_config(endpoint);
969 }
970
971 fn add_metrics(
972 &self,
973 endpoint: &crate::component::Endpoint,
974 metrics_labels: Option<&[(&str, &str)]>,
975 ) -> Result<()> {
976 Ingress::add_metrics(self, endpoint, metrics_labels)
978 }
979
980 fn set_endpoint_health_check_notifier(&self, notifier: Arc<tokio::sync::Notify>) -> Result<()> {
981 self.endpoint_health_check_notifier
982 .set(notifier)
983 .map_err(|_| anyhow::anyhow!("Endpoint health check notifier already set"))?;
984 Ok(())
985 }
986
987 async fn handle_payload(
988 &self,
989 payload: Bytes,
990 request_id: Option<String>,
991 ) -> Result<(), PipelineError> {
992 self.handle_payload_shared(payload, request_id).await
993 }
994}
995
996#[async_trait]
997impl<T, U, Adapter> PushWorkHandler for Ingress<ManyIn<T>, ManyOut<U>, Adapter>
998where
999 T: Data + for<'de> Deserialize<'de> + std::fmt::Debug,
1000 U: Data + std::fmt::Debug,
1001 Adapter: IngressPayloadAdapter<T, U> + Send + Sync + 'static,
1002{
1003 fn bind_endpoint(&self, endpoint: &crate::component::Endpoint) {
1004 self.bind_endpoint_config(endpoint);
1005 }
1006
1007 fn add_metrics(
1008 &self,
1009 endpoint: &crate::component::Endpoint,
1010 metrics_labels: Option<&[(&str, &str)]>,
1011 ) -> Result<()> {
1012 Ingress::add_metrics(self, endpoint, metrics_labels)
1014 }
1015
1016 fn set_endpoint_health_check_notifier(&self, notifier: Arc<tokio::sync::Notify>) -> Result<()> {
1017 self.endpoint_health_check_notifier
1018 .set(notifier)
1019 .map_err(|_| anyhow::anyhow!("Endpoint health check notifier already set"))?;
1020 Ok(())
1021 }
1022
1023 async fn handle_payload(
1024 &self,
1025 payload: Bytes,
1026 request_id: Option<String>,
1027 ) -> Result<(), PipelineError> {
1028 self.handle_payload_shared(payload, request_id).await
1029 }
1030}
1031
1032pub(crate) fn typed_error_from_pipeline_error(e: &PipelineError) -> DynamoError {
1040 let source: &(dyn std::error::Error + 'static) = match e {
1041 PipelineError::GenerateError(inner) => inner.as_ref(),
1042 other => other,
1043 };
1044 DynamoError::from(source)
1045}
1046
1047#[cfg(test)]
1048mod tests {
1049 use super::*;
1050 use crate::pipeline::network::{Ingress, RequestPlanePayloadCodec, StreamSender};
1051 use crate::pipeline::{Context, ManyOut, ResponseStream, SingleIn};
1052 use crate::protocols::annotated::Annotated;
1053 use futures::stream;
1054 use prometheus::{Histogram, HistogramOpts, IntCounter, IntCounterVec, IntGauge, Opts};
1055 use std::sync::atomic::{AtomicBool, Ordering};
1056
1057 use crate::error::{BackendError, ErrorType};
1058
1059 type TestRequest = serde_json::Value;
1060 type TestResponse = Annotated<serde_json::Value>;
1061 type TestIngress = Ingress<SingleIn<TestRequest>, ManyOut<TestResponse>>;
1062
1063 #[test]
1067 fn generate_error_payload_keeps_the_workers_error_type() {
1068 let e = PipelineError::GenerateError(anyhow::Error::new(
1069 DynamoError::builder()
1070 .error_type(ErrorType::Backend(BackendError::InvalidArgument))
1071 .message("multimodal input is not supported by this backend")
1072 .build(),
1073 ));
1074
1075 assert_eq!(
1076 typed_error_from_pipeline_error(&e).error_type(),
1077 ErrorType::Backend(BackendError::InvalidArgument),
1078 "the worker's type must survive the anyhow payload"
1079 );
1080 }
1081
1082 #[test]
1085 fn non_generate_pipeline_error_is_internal_unclassified() {
1086 let e = PipelineError::DeserializationError("bad request payload".to_string());
1087 let error = typed_error_from_pipeline_error(&e);
1088
1089 assert_eq!(error.class(), ErrorType::Internal);
1090 assert_eq!(error.reason().as_str(), "runtime.unclassified");
1091 }
1092
1093 #[derive(Clone, Default)]
1094 struct AdmissionCapture {
1095 started: Arc<AtomicBool>,
1096 closed: Arc<AtomicBool>,
1097 }
1098
1099 impl<S> tracing_subscriber::Layer<S> for AdmissionCapture
1100 where
1101 S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
1102 {
1103 fn on_new_span(
1104 &self,
1105 attrs: &tracing::span::Attributes<'_>,
1106 _id: &tracing::span::Id,
1107 _ctx: tracing_subscriber::layer::Context<'_, S>,
1108 ) {
1109 if attrs.metadata().name() == "worker.admission" {
1110 self.started.store(true, Ordering::SeqCst);
1111 }
1112 }
1113
1114 fn on_close(&self, id: tracing::span::Id, ctx: tracing_subscriber::layer::Context<'_, S>) {
1115 if ctx.span(&id).unwrap().metadata().name() == "worker.admission" {
1116 self.closed.store(true, Ordering::SeqCst);
1117 }
1118 }
1119 }
1120
1121 struct AdmissionProbe(AdmissionCapture, bool);
1122
1123 #[async_trait]
1124 impl crate::engine::AsyncEngine<SingleIn<TestRequest>, ManyOut<TestResponse>, anyhow::Error>
1125 for AdmissionProbe
1126 {
1127 async fn generate(
1128 &self,
1129 _request: SingleIn<TestRequest>,
1130 ) -> anyhow::Result<ManyOut<TestResponse>> {
1131 assert_eq!(self.0.started.load(Ordering::SeqCst), self.1);
1132 assert_eq!(
1133 self.0.closed.load(Ordering::SeqCst),
1134 self.1,
1135 "admission must close before generation, while the TCP reader is still alive"
1136 );
1137 anyhow::bail!("admission probe finished")
1138 }
1139 }
1140
1141 #[tokio::test]
1142 async fn lifecycle_admission_closes_before_generation_and_skips_control_calls() {
1143 use tracing::instrument::WithSubscriber;
1144 use tracing_subscriber::prelude::*;
1145
1146 const CHILD: &str = "DYNAMO_LIFECYCLE_ADMISSION_TEST_CHILD";
1149 if std::env::var_os(CHILD).is_none() {
1150 let status = std::process::Command::new(std::env::current_exe().unwrap())
1151 .args([
1152 "--exact",
1153 "pipeline::network::ingress::push_handler::tests::lifecycle_admission_closes_before_generation_and_skips_control_calls",
1154 "--nocapture",
1155 ])
1156 .env(CHILD, "1")
1157 .env("DYN_LIFECYCLE_TRACE_ENABLED", "true")
1158 .status()
1159 .unwrap();
1160 assert!(status.success(), "isolated admission regression failed");
1161 return;
1162 }
1163
1164 temp_env::async_with_vars(
1165 [
1166 ("DYN_LIFECYCLE_TRACE_ENABLED", Some("true")),
1167 ("DYN_RESPONSE_PLANE", Some("tcp")),
1168 ],
1169 async {
1170 for (inference, rooted) in
1171 [(true, true), (true, false), (false, true), (false, false)]
1172 {
1173 let capture = AdmissionCapture::default();
1174 let subscriber = tracing_subscriber::registry().with(capture.clone());
1175 async {
1176 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1177 let address = listener.local_addr().unwrap().to_string();
1178 let peer = tokio::spawn(async move {
1179 let (mut socket, _) = listener.accept().await.unwrap();
1180 let _ = tokio::io::copy(&mut socket, &mut tokio::io::sink()).await;
1181 });
1182 let engine = Arc::new(AdmissionProbe(capture, inference && rooted));
1183 let ingress = if inference {
1184 TestIngress::for_engine_with_lifecycle_role(
1185 engine,
1186 crate::telemetry::LifecycleOperationRole::Worker,
1187 )
1188 } else {
1189 TestIngress::for_engine(engine)
1190 }
1191 .unwrap();
1192 let connection: ConnectionInfo = tcp::TcpStreamConnectionInfo {
1193 address,
1194 subject: "admission-probe".to_string(),
1195 context: "admission-probe".to_string(),
1196 stream_type: crate::pipeline::network::StreamType::Response,
1197 }
1198 .into();
1199 let metadata = if rooted {
1200 std::collections::BTreeMap::from([(
1201 crate::telemetry::LIFECYCLE_ROOT_METADATA_KEY,
1202 "v1",
1203 )])
1204 } else {
1205 std::collections::BTreeMap::new()
1206 };
1207 let header = serde_json::to_vec(&serde_json::json!({
1208 "id": "admission-probe",
1209 "request_type": "single_in",
1210 "response_type": "many_out",
1211 "connection_info": connection,
1212 "metadata": metadata,
1213 }))
1214 .unwrap();
1215 let payload = TwoPartCodec::default()
1216 .encode_message(TwoPartMessage::from_parts(
1217 header.into(),
1218 Bytes::from_static(b"{}"),
1219 ))
1220 .unwrap();
1221 let error = tokio::time::timeout(
1222 std::time::Duration::from_secs(10),
1223 ingress
1224 .handle_payload_shared(payload, Some("admission-probe".to_string()))
1225 .instrument(tracing::info_span!("handle_payload")),
1226 )
1227 .await
1228 .expect("local admission probe timed out")
1229 .unwrap_err();
1230 assert!(error.to_string().contains("admission probe finished"));
1231 peer.abort();
1232 let _ = peer.await;
1233 }
1234 .with_subscriber(subscriber)
1235 .await;
1236 }
1237 },
1238 )
1239 .await;
1240 }
1241
1242 #[derive(Default)]
1243 struct MismatchPublisher {
1244 prologue: Arc<std::sync::Mutex<Option<Option<String>>>>,
1245 finished: Arc<AtomicBool>,
1246 }
1247
1248 impl ResponsePublisher for MismatchPublisher {
1249 async fn send(&self, _payload: Bytes) -> anyhow::Result<()> {
1250 panic!("mismatch must not send response data")
1251 }
1252
1253 async fn send_prologue(&mut self, error: Option<String>) -> anyhow::Result<()> {
1254 *self.prologue.lock().unwrap() = Some(error);
1255 Ok(())
1256 }
1257
1258 async fn finish(&mut self) -> anyhow::Result<()> {
1259 self.finished.store(true, Ordering::Release);
1260 Ok(())
1261 }
1262
1263 async fn abort(&mut self) -> anyhow::Result<()> {
1264 panic!("mismatch must finish with a frontend-visible error")
1265 }
1266 }
1267
1268 #[tokio::test]
1269 async fn response_plane_mismatch_reports_error_before_generate() {
1270 for (configured, advertised) in [
1271 (ResponsePlaneMode::Tcp, ResponsePlaneMode::Quic),
1272 (ResponsePlaneMode::Quic, ResponsePlaneMode::Tcp),
1273 ] {
1274 let ingress = TestIngress::new();
1275 let publisher = MismatchPublisher::default();
1276 let prologue = publisher.prologue.clone();
1277 let finished = publisher.finished.clone();
1278 let lifecycle = LifecycleTrace::new(false);
1279
1280 let error = ingress
1281 .generate_and_publish(
1282 Context::new(serde_json::json!({})),
1283 RequestPlanePayloadCodec::Json,
1284 Instant::now(),
1285 ResponsePlaneModes {
1286 configured,
1287 advertised,
1288 },
1289 &lifecycle,
1290 publisher,
1291 )
1292 .await
1293 .expect_err("mismatched response planes must fail");
1294
1295 let expected = format!(
1296 "response plane mismatch: frontend requested {}, worker configured {}",
1297 advertised.name(),
1298 configured.name()
1299 );
1300 assert!(error.to_string().contains(&expected));
1301 assert_eq!(*prologue.lock().unwrap(), Some(Some(expected)));
1302 assert!(finished.load(Ordering::Acquire));
1303 }
1304 }
1305
1306 fn test_metrics() -> WorkHandlerMetrics {
1308 WorkHandlerMetrics::new(
1309 IntCounter::with_opts(Opts::new("requests_total", "t")).unwrap(),
1310 Histogram::with_opts(HistogramOpts::new("request_duration_seconds", "t")).unwrap(),
1311 IntGauge::with_opts(Opts::new("inflight_requests", "t")).unwrap(),
1312 IntCounter::with_opts(Opts::new("request_bytes_total", "t")).unwrap(),
1313 IntCounter::with_opts(Opts::new("response_bytes_total", "t")).unwrap(),
1314 IntCounterVec::new(
1315 Opts::new(work_handler::ERRORS_TOTAL, "t"),
1316 &[work_handler::ERROR_TYPE_LABEL],
1317 )
1318 .unwrap(),
1319 IntCounter::with_opts(Opts::new("cancellation_total", "t")).unwrap(),
1320 )
1321 }
1322
1323 #[test]
1324 fn test_quic_client_pool_initializes_without_add_metrics() {
1325 let ingress = TestIngress::new();
1326 assert!(ingress.metrics().is_none());
1327
1328 let first = ingress.quic_response_client_pool().unwrap();
1329 let second = ingress.quic_response_client_pool().unwrap();
1330 assert!(Arc::ptr_eq(&first, &second));
1331 }
1332
1333 #[derive(Clone, Copy, Debug)]
1335 enum Teardown {
1336 Stop,
1339 Kill,
1342 ConnectionReadError,
1347 TransportOnly,
1350 }
1351
1352 async fn run_teardown_race(content_frames: usize, teardown: Teardown) -> (u64, u64) {
1359 let ingress = TestIngress::new();
1360 let metrics = Arc::new(test_metrics());
1361 ingress
1362 .metrics
1363 .set(metrics.clone())
1364 .expect("metrics already set");
1365
1366 let (tx, mut rx) = tokio::sync::mpsc::channel(content_frames + 8);
1369 let publisher = StreamSender { tx, prologue: None };
1370
1371 let ctx = Context::new(serde_json::json!({}));
1372 let engine_ctx = ctx.context();
1373
1374 let (gate_tx, gate_rx) = tokio::sync::oneshot::channel::<()>();
1378 let content: Vec<TestResponse> = (0..content_frames)
1379 .map(|i| Annotated::from_data(serde_json::json!({ "token": i })))
1380 .collect();
1381 let tail = stream::unfold(Some(gate_rx), |state| async move {
1382 let gate = state?;
1385 let _ = gate.await;
1386 None
1387 });
1388 let response_stream: ManyOut<TestResponse> = ResponseStream::new(
1389 Box::pin(stream::iter(content).chain(tail)),
1390 engine_ctx.clone(),
1391 );
1392
1393 let pump = tokio::spawn({
1394 let ingress = ingress.clone();
1395 async move {
1396 ingress
1397 .pump_response_stream(
1398 response_stream,
1399 &publisher,
1400 RequestPlanePayloadCodec::Json,
1401 )
1402 .await;
1403 }
1404 });
1405
1406 for _ in 0..content_frames {
1408 rx.recv().await.expect("content frame");
1409 }
1410
1411 match teardown {
1414 Teardown::Stop => engine_ctx.stop(),
1415 Teardown::Kill | Teardown::ConnectionReadError => engine_ctx.kill(),
1416 Teardown::TransportOnly => {}
1417 }
1418 drop(rx);
1419 let _ = gate_tx.send(());
1420
1421 pump.await.expect("pump task panicked");
1422
1423 let errors = &metrics.error_counter;
1424 (
1425 errors
1426 .with_label_values(&[work_handler::error_types::PUBLISH_FINAL])
1427 .get(),
1428 errors
1429 .with_label_values(&[work_handler::error_types::PUBLISH_RESPONSE])
1430 .get(),
1431 )
1432 }
1433
1434 #[tokio::test]
1438 async fn test_publish_final_race_with_stopped_context_is_not_an_error() {
1439 let (publish_final, publish_response) = run_teardown_race(3, Teardown::Stop).await;
1440 assert_eq!(
1441 publish_final, 0,
1442 "complete_final lost to a stopped context must not count as an error"
1443 );
1444 assert_eq!(
1445 publish_response, 0,
1446 "content frames were all delivered before teardown"
1447 );
1448 }
1449
1450 #[tokio::test]
1456 async fn test_publish_final_with_killed_context_is_still_an_error() {
1457 let (publish_final, _) = run_teardown_race(3, Teardown::Kill).await;
1458 assert_eq!(
1459 publish_final, 1,
1460 "a killed context is not a graceful teardown and must still be counted"
1461 );
1462 }
1463
1464 #[tokio::test]
1468 async fn test_publish_final_after_connection_read_error_is_still_an_error() {
1469 let (publish_final, _) = run_teardown_race(3, Teardown::ConnectionReadError).await;
1470 assert_eq!(
1471 publish_final, 1,
1472 "a dropped connection must not be silently reclassified as a benign teardown"
1473 );
1474 }
1475
1476 #[tokio::test]
1479 async fn test_publish_final_failure_without_stop_is_still_an_error() {
1480 let (publish_final, _) = run_teardown_race(3, Teardown::TransportOnly).await;
1481 assert_eq!(
1482 publish_final, 1,
1483 "a genuine complete_final failure must still be counted"
1484 );
1485 }
1486
1487 #[tokio::test]
1494 async fn test_complete_final_marker_still_sent_on_clean_stream() {
1495 let ingress = TestIngress::new();
1496 let metrics = Arc::new(test_metrics());
1497 ingress.metrics.set(metrics.clone()).unwrap();
1498
1499 let content_frames = 3;
1500 let (tx, mut rx) = tokio::sync::mpsc::channel(content_frames + 8);
1501 let publisher = StreamSender { tx, prologue: None };
1502
1503 let ctx = Context::new(serde_json::json!({}));
1504 let content: Vec<TestResponse> = (0..content_frames)
1505 .map(|i| Annotated::from_data(serde_json::json!({ "token": i })))
1506 .collect();
1507 let response_stream: ManyOut<TestResponse> =
1508 ResponseStream::new(Box::pin(stream::iter(content)), ctx.context());
1509
1510 ingress
1511 .pump_response_stream(response_stream, &publisher, RequestPlanePayloadCodec::Json)
1512 .await;
1513 drop(publisher);
1514
1515 let mut frames = Vec::new();
1516 while let Some(msg) = rx.recv().await {
1517 let (_header, data) = msg.into_parts();
1518 frames.push(serde_json::from_slice::<serde_json::Value>(&data).unwrap());
1519 }
1520
1521 assert_eq!(
1522 frames.len(),
1523 content_frames + 1,
1524 "expected every content frame plus the trailing marker"
1525 );
1526 for (i, frame) in frames.iter().take(content_frames).enumerate() {
1527 assert_eq!(frame["complete_final"], false, "content frame {i}");
1528 }
1529 assert_eq!(
1530 frames[content_frames]["complete_final"], true,
1531 "trailing frame must carry the end-of-stream marker"
1532 );
1533
1534 let errors = &metrics.error_counter;
1535 assert_eq!(
1536 errors
1537 .with_label_values(&[work_handler::error_types::PUBLISH_FINAL])
1538 .get(),
1539 0
1540 );
1541 assert_eq!(
1542 errors
1543 .with_label_values(&[work_handler::error_types::PUBLISH_RESPONSE])
1544 .get(),
1545 0
1546 );
1547 }
1548}