1use std::collections::BTreeMap;
5use std::future::Future;
6use std::sync::{Arc, LazyLock};
7use std::time::Instant;
8
9use super::unified_client::RequestPlaneClient;
10use super::*;
11use crate::component::Instance;
12use crate::discovery::EndpointInstanceId;
13use crate::dynamo_nvtx_range;
14use crate::engine::{AsyncEngine, AsyncEngineContextProvider, Data, EngineContextGuard};
15use crate::error::{DynamoError, ErrorType, match_error_chain};
16use crate::logging::inject_trace_headers_into_map;
17use crate::metrics::frontend_perf::STAGE_DURATION_SECONDS;
18use crate::metrics::request_plane::{
19 REQUEST_PLANE_INFLIGHT, REQUEST_PLANE_QUEUE_SECONDS, REQUEST_PLANE_ROUNDTRIP_TTFT_SECONDS,
20 REQUEST_PLANE_SEND_SECONDS,
21};
22use crate::pipeline::network::ConnectionInfo;
23use crate::pipeline::network::NetworkStreamWrapper;
24use crate::pipeline::network::PendingConnections;
25use crate::pipeline::network::RegisteredStream;
26use crate::pipeline::network::RequestControlMessage;
27use crate::pipeline::network::RequestPlanePayloadCodec;
28use crate::pipeline::network::RequestType;
29use crate::pipeline::network::ResponseType;
30use crate::pipeline::network::StreamOptions;
31use crate::pipeline::network::StreamPrologueError;
32use crate::pipeline::network::StreamProvider;
33use crate::pipeline::network::StreamReceiver;
34use crate::pipeline::network::StreamSender;
35use crate::pipeline::network::TwoPartCodec;
36use crate::pipeline::network::codec::TwoPartMessage;
37use crate::pipeline::network::tcp;
38use crate::pipeline::{ManyIn, ManyOut, PipelineError, ResponseStream, SingleIn};
39use crate::protocols::maybe_error::MaybeError;
40use crate::traits::DistributedRuntimeProvider;
41
42use anyhow::{Error, Result};
43use futures::stream::Stream;
44use parking_lot::Mutex;
45use std::pin::Pin;
46use std::task::{Context, Poll};
47use tokio::sync::{OwnedSemaphorePermit, Semaphore};
48use tokio_stream::{StreamExt, StreamNotifyClose, wrappers::ReceiverStream};
49use tracing::Instrument;
50
51pub(crate) const MIGRATION_SENSITIVE_ERROR_TYPES: &[ErrorType] =
60 &[ErrorType::Cancelled, ErrorType::ResourceExhausted];
61
62fn is_migration_sensitive(err: &DynamoError) -> bool {
69 match_error_chain(err, MIGRATION_SENSITIVE_ERROR_TYPES, &[])
70}
71
72pub(crate) fn pre_stream_failure_error(error: StreamPrologueError) -> DynamoError {
84 let builder = DynamoError::builder()
85 .error_type(ErrorType::CannotConnect)
86 .message(format!(
87 "Worker generate() failed before response stream: {error}"
88 ));
89
90 match error.typed_error {
91 Some(typed) if !is_migration_sensitive(&typed) => builder.cause(typed).build(),
92 _ => builder.build(),
93 }
94}
95
96#[cfg(any(test, feature = "testing"))]
99#[doc(hidden)]
100pub mod testing {
101 use super::{DynamoError, ErrorType, StreamPrologueError};
102
103 pub fn migration_sensitive_error_types() -> &'static [ErrorType] {
106 super::MIGRATION_SENSITIVE_ERROR_TYPES
107 }
108
109 pub fn pre_stream_failure_error(error: StreamPrologueError) -> DynamoError {
110 super::pre_stream_failure_error(error)
111 }
112}
113
114const FIRST_RESPONSE_GUARD_CONTEXT_KEY: &str = "dynamo.request_plane.first_response_guard";
115const MAX_RETAINED_FIRST_RESPONSE_DISPATCHES: usize = 1024;
118static RETAINED_FIRST_RESPONSE_DISPATCH_PERMITS: LazyLock<Arc<Semaphore>> =
119 LazyLock::new(|| Arc::new(Semaphore::new(MAX_RETAINED_FIRST_RESPONSE_DISPATCHES)));
120
121#[derive(Clone)]
122struct FirstResponseGuard {
123 guard: Arc<Mutex<Option<EngineContextGuard>>>,
124}
125
126impl FirstResponseGuard {
127 fn new(guard: EngineContextGuard) -> Self {
128 Self {
129 guard: Arc::new(Mutex::new(Some(guard))),
130 }
131 }
132
133 fn take(&self) -> Option<EngineContextGuard> {
134 self.guard.lock().take()
135 }
136}
137
138pub fn attach_first_response_guard<T: Data>(
141 context: &mut context::Context<T>,
142 guard: EngineContextGuard,
143) {
144 context.insert(
145 FIRST_RESPONSE_GUARD_CONTEXT_KEY,
146 FirstResponseGuard::new(guard),
147 );
148}
149
150pub fn propagate_first_response_guard<S: Data, T: Data>(
152 source: &context::Context<S>,
153 target: &mut context::Context<T>,
154) -> Result<(), Error> {
155 if let Some(guard) = source
156 .get_optional::<FirstResponseGuard>(FIRST_RESPONSE_GUARD_CONTEXT_KEY)
157 .map_err(Error::msg)?
158 {
159 target.insert(FIRST_RESPONSE_GUARD_CONTEXT_KEY, guard.as_ref().clone());
160 }
161 Ok(())
162}
163
164fn try_acquire_retained_dispatch_permit(
165 permits: &Arc<Semaphore>,
166) -> Result<OwnedSemaphorePermit, Error> {
167 permits.clone().try_acquire_owned().map_err(|_| {
168 DynamoError::builder()
169 .error_type(ErrorType::ResourceExhausted)
170 .message("retained request dispatch limit reached")
171 .build()
172 .into()
173 })
174}
175
176async fn dispatch_with_first_response_guard<F, U>(
179 dispatch: F,
180 guard: EngineContextGuard,
181 permit: OwnedSemaphorePermit,
182) -> Result<ManyOut<U>, Error>
183where
184 F: Future<Output = Result<ManyOut<U>, Error>> + Send + 'static,
185 U: Data + MaybeError,
186{
187 let (dispatch_tx, dispatch_rx) = tokio::sync::oneshot::channel();
188
189 tokio::spawn(
190 async move {
191 let mut response = match dispatch.await {
192 Ok(response) => response,
193 Err(error) => {
194 let _ = dispatch_tx.send(Err(error));
195 return;
196 }
197 };
198
199 let response_context = response.context();
200 let (first_tx, first_rx) = tokio::sync::oneshot::channel::<(Option<U>, ManyOut<U>)>();
201 let stream = async_stream::stream! {
202 match first_rx.await {
203 Ok((first, mut tail)) => {
204 if let Some(first) = first {
205 yield first;
206 }
207 while let Some(item) = tail.next().await {
208 yield item;
209 }
210 }
211 Err(_) => {
212 yield U::from_err(DynamoError::msg(
213 "retained request dispatch ended before first response handoff",
214 ));
215 }
216 }
217 };
218 let handoff: ManyOut<U> = ResponseStream::new(Box::pin(stream), response_context);
219 let _ = dispatch_tx.send(Ok(handoff));
220
221 let first = response.next().await;
222 drop(guard);
223 drop(permit);
224 let _ = first_tx.send((first, response));
225 }
226 .in_current_span(),
227 );
228
229 dispatch_rx
230 .await
231 .map_err(|_| anyhow::anyhow!("retained request dispatch ended before setup completed"))?
232}
233
234fn decode_response_stream<U>(
240 response_rx: tokio::sync::mpsc::Receiver<bytes::Bytes>,
241 engine_ctx: Arc<dyn crate::engine::AsyncEngineContext>,
242 queue_start: Instant,
243 tx_start: Instant,
244 inflight_guard: InflightGuard,
245 payload_codec: RequestPlanePayloadCodec,
246) -> ManyOut<U>
247where
248 U: Data + for<'de> Deserialize<'de> + MaybeError,
249{
250 let engine_ctx_for_stream = engine_ctx.clone();
251 let mut is_complete_final = false;
252 let mut first_response = true;
253 let stream = StreamNotifyClose::new(ReceiverStream::new(response_rx)).filter_map(move |res| {
254 if let Some(res_bytes) = res {
255 if first_response {
256 first_response = false;
257 REQUEST_PLANE_ROUNDTRIP_TTFT_SECONDS.observe(tx_start.elapsed().as_secs_f64());
258 STAGE_DURATION_SECONDS
259 .with_label_values(&["transport_roundtrip"])
260 .observe(queue_start.elapsed().as_secs_f64());
261 }
262 if is_complete_final {
263 let err = DynamoError::msg(
264 "Response received after generation ended - this should never happen",
265 );
266 return Some(U::from_err(err));
267 }
268 match payload_codec.decode::<NetworkStreamWrapper<U>>(&res_bytes) {
269 Ok(item) => {
270 is_complete_final = item.complete_final;
271 if let Some(data) = item.data {
272 Some(data)
273 } else if is_complete_final {
274 None
275 } else {
276 let err =
277 DynamoError::msg("Empty response received - this should never happen");
278 Some(U::from_err(err))
279 }
280 }
281 Err(err) => {
282 let response_bytes_len = res_bytes.len();
283 tracing::warn!(
284 %err,
285 codec = payload_codec.name(),
286 response_bytes_len,
287 "failed deserializing request-plane response"
288 );
289 Some(U::from_err(DynamoError::msg(err.to_string())))
290 }
291 }
292 } else if is_complete_final {
293 None
294 } else if engine_ctx_for_stream.is_stopped() {
295 tracing::debug!("Request cancelled and then trying to read a response");
296 None
297 } else {
298 let err = DynamoError::builder()
299 .error_type(ErrorType::Disconnected)
300 .message("Stream ended before generation completed")
301 .build();
302 tracing::debug!("{err}");
303 Some(U::from_err(err))
304 }
305 });
306
307 inflight_guard.disarm();
308 let stream = InflightDecStream { inner: stream };
309 ResponseStream::new(Box::pin(stream), engine_ctx)
310}
311
312const CONTROL_MESSAGE_MAX_BYTES: usize = 128 * 1024;
313
314fn serialize_control_message(control_message: &RequestControlMessage) -> Result<Vec<u8>, Error> {
315 let ctrl = serde_json::to_vec(control_message)?;
316 if ctrl.len() > CONTROL_MESSAGE_MAX_BYTES {
317 return Err(PipelineError::Generic(format!(
318 "request control message too large: {} bytes exceeds limit {}",
319 ctrl.len(),
320 CONTROL_MESSAGE_MAX_BYTES
321 ))
322 .into());
323 }
324 Ok(ctrl)
325}
326
327fn build_request_envelope<T>(
334 context: &context::Context<()>,
335 recv_conn_info: ConnectionInfo,
336 send_conn_info: Option<ConnectionInfo>,
337 request: Option<&T>,
338 payload_codec: RequestPlanePayloadCodec,
339) -> Result<bytes::Bytes, Error>
340where
341 T: serde::Serialize,
342{
343 let request_id = context.id();
344 let request_type = if send_conn_info.is_some() {
345 RequestType::ManyIn
346 } else {
347 RequestType::SingleIn
348 };
349 let control_message = RequestControlMessage {
350 id: request_id.to_string(),
351 request_type,
352 response_type: ResponseType::ManyOut,
353 payload_codec,
354 connection_info: recv_conn_info,
355 metadata: context.metadata().clone(),
356 frontend_send_ts_ns: None,
357 request_stream_connection_info: send_conn_info,
358 };
359
360 let ctrl = serialize_control_message(&control_message)?;
361 let data: Option<Vec<u8>> = match request {
362 Some(req) => Some(payload_codec.encode(req)?),
363 None => None,
364 };
365
366 let msg = match data {
367 Some(d) => {
368 tracing::trace!(
369 request_id,
370 "packaging two-part message; ctrl: {} bytes, data: {} bytes",
371 ctrl.len(),
372 d.len(),
373 );
374 TwoPartMessage::from_parts(ctrl.into(), d.into())
375 }
376 None => {
377 tracing::trace!(
378 request_id,
379 "packaging bidirectional header-only envelope; ctrl: {} bytes",
380 ctrl.len(),
381 );
382 TwoPartMessage::from_header(ctrl.into())
383 }
384 };
385
386 let codec = TwoPartCodec::default();
387 let buffer = codec.encode_message(msg)?;
388 Ok(buffer)
389}
390
391fn payload_codec_for_worker(instance: Option<&Instance>) -> RequestPlanePayloadCodec {
392 instance
393 .and_then(|instance| instance.request_plane_codec)
394 .unwrap_or(RequestPlanePayloadCodec::Json)
395}
396
397async fn spawn_request_stream_forwarder<T>(
402 request_stream_provider: Option<StreamProvider<StreamSender>>,
403 mut input_stream: crate::engine::DataStream<T>,
404 engine_ctx: Arc<dyn crate::engine::AsyncEngineContext>,
405 payload_codec: RequestPlanePayloadCodec,
406) -> Result<(), Error>
407where
408 T: serde::Serialize + Send + 'static,
409{
410 let Some(provider) = request_stream_provider else {
411 return Ok(());
412 };
413
414 let request_sender = match provider.await {
415 Ok(Ok(sender)) => sender,
416 Ok(Err(e)) => {
417 return Err(anyhow::anyhow!(
418 DynamoError::builder()
419 .error_type(ErrorType::CannotConnect)
420 .message(format!("Worker dial-in failed for request stream: {e}"))
421 .build()
422 ));
423 }
424 Err(_) => {
425 return Err(anyhow::anyhow!(
426 DynamoError::builder()
427 .error_type(ErrorType::Disconnected)
428 .message("Worker disconnected before request stream was established")
429 .build()
430 ));
431 }
432 };
433
434 tokio::spawn(async move {
440 loop {
441 let item = tokio::select! {
442 biased;
443 _ = engine_ctx.killed() => break,
444 _ = engine_ctx.stopped() => break,
445 item = input_stream.next() => match item {
446 Some(item) => item,
447 None => break,
448 },
449 };
450 let bytes = match payload_codec.encode(&item) {
451 Ok(b) => b,
452 Err(e) => {
453 tracing::error!(
458 error = %e,
459 codec = payload_codec.name(),
460 "failed to serialize bidirectional request frame; killing context"
461 );
462 engine_ctx.kill();
463 break;
464 }
465 };
466 if request_sender.send(bytes.into()).await.is_err() {
467 tracing::debug!("worker request-stream receiver dropped; forwarder exiting");
468 break;
469 }
470 }
471 });
472
473 Ok(())
474}
475
476struct InflightGuard {
480 armed: bool,
481}
482
483impl InflightGuard {
484 fn new() -> Self {
485 Self { armed: true }
486 }
487
488 fn disarm(mut self) {
491 self.armed = false;
492 }
493}
494
495impl Drop for InflightGuard {
496 fn drop(&mut self) {
497 if self.armed {
498 REQUEST_PLANE_INFLIGHT.dec();
499 }
500 }
501}
502
503struct InflightDecStream<S> {
505 inner: S,
506}
507
508impl<S, T> Stream for InflightDecStream<S>
509where
510 S: Stream<Item = T> + Unpin,
511{
512 type Item = T;
513
514 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
515 Pin::new(&mut self.inner).poll_next(cx)
516 }
517}
518
519impl<S> Drop for InflightDecStream<S> {
520 fn drop(&mut self) {
521 REQUEST_PLANE_INFLIGHT.dec();
522 }
523}
524
525fn subject_of(conn_info: &ConnectionInfo) -> Option<String> {
529 serde_json::from_str::<tcp::TcpStreamConnectionInfo>(&conn_info.info)
530 .ok()
531 .map(|ci| ci.subject)
532}
533
534pub struct AddressedRequest<T> {
535 request: T,
536 address: String,
537 instance: Option<Instance>,
540}
541
542impl<T> AddressedRequest<T> {
543 pub fn new(request: T, address: String) -> Self {
544 Self {
545 request,
546 address,
547 instance: None,
548 }
549 }
550
551 pub fn with_instance(request: T, address: String, instance: Instance) -> Self {
552 Self {
553 request,
554 address,
555 instance: Some(instance),
556 }
557 }
558
559 pub fn for_instance(request: T, instance: Instance) -> Self {
560 let address = instance.transport.address().to_string();
561 Self::with_instance(request, address, instance)
562 }
563
564 pub fn into_parts(self) -> (T, String, Option<Instance>) {
567 (self.request, self.address, self.instance)
568 }
569}
570
571#[derive(Clone)]
572pub struct AddressedPushRouter {
573 req_client: Arc<dyn RequestPlaneClient>,
575
576 resp_transport: Arc<tcp::server::TcpStreamServer>,
578}
579
580impl AddressedPushRouter {
581 pub fn new(
586 req_client: Arc<dyn RequestPlaneClient>,
587 resp_transport: Arc<tcp::server::TcpStreamServer>,
588 ) -> Result<Arc<Self>> {
589 Ok(Arc::new(Self {
590 req_client,
591 resp_transport,
592 }))
593 }
594
595 pub async fn from_runtime_provider(
596 provider: &impl DistributedRuntimeProvider,
597 ) -> Result<Arc<Self>> {
598 let manager = provider.drt().network_manager();
599 let req_client = manager.create_client()?;
600 let resp_transport = provider.drt().tcp_server().await?;
601
602 tracing::debug!(
603 transport = req_client.transport_name(),
604 "Creating AddressedPushRouter with request plane client"
605 );
606
607 Self::new(req_client, resp_transport)
608 }
609
610 pub async fn cancel_instance_streams(&self, instance_id: &EndpointInstanceId) -> usize {
612 self.resp_transport
613 .cancel_instance_streams(instance_id)
614 .await
615 }
616
617 pub async fn clear_instance_tombstone(&self, instance_id: &EndpointInstanceId) {
619 self.resp_transport
620 .clear_instance_tombstone(instance_id)
621 .await
622 }
623
624 pub async fn dispatch_bidirectional<T, U>(
630 &self,
631 instance: Instance,
632 address: String,
633 input: ManyIn<T>,
634 ) -> Result<ManyOut<U>, Error>
635 where
636 T: Data + Serialize,
637 U: Data + for<'de> Deserialize<'de> + MaybeError,
638 {
639 let (request_stream, context) = input.into_parts();
640 let input_stream = request_stream.take().ok_or_else(|| {
641 anyhow::anyhow!("RequestStream::take called twice on bidirectional dispatch input")
642 })?;
643
644 self.dispatch_and_finalize::<T, U>(
645 &context,
646 address,
647 Some(&instance),
648 None,
649 Some(input_stream),
650 )
651 .await
652 }
653
654 async fn dispatch_and_finalize<T, U>(
662 &self,
663 context: &context::Context<()>,
664 address: String,
665 instance: Option<&Instance>,
666 request: Option<&T>,
667 input_stream: Option<crate::engine::DataStream<T>>,
668 ) -> Result<ManyOut<U>, Error>
669 where
670 T: Data + Serialize,
671 U: Data + for<'de> Deserialize<'de> + MaybeError,
672 {
673 let engine_ctx = context.context();
674
675 let queue_start = Instant::now();
676 REQUEST_PLANE_INFLIGHT.inc();
677 let inflight_guard = InflightGuard::new();
678
679 let enable_request_stream = input_stream.is_some();
680 let payload_codec = payload_codec_for_worker(instance);
681
682 let (send_registered, recv_registered) = self
688 .register_streams(engine_ctx.clone(), enable_request_stream, true)
689 .await?;
690 let recv_registered = recv_registered.ok_or_else(|| {
691 anyhow::anyhow!("response stream registration missing despite enable_response_stream")
692 })?;
693
694 let recv_subject = subject_of(&recv_registered.connection_info);
698 let send_subject = send_registered
699 .as_ref()
700 .and_then(|r| subject_of(&r.connection_info));
701 if let (Some(subject), Some(inst)) = (&recv_subject, instance)
702 && !self
703 .resp_transport
704 .associate_instance(
705 subject,
706 send_subject.as_deref(),
707 &inst.endpoint_instance_id(),
708 )
709 .await
710 {
711 return Err(anyhow::anyhow!(
712 DynamoError::builder()
713 .error_type(ErrorType::Disconnected)
714 .message("Worker removed before request could be sent (tombstoned instance)")
715 .build()
716 ));
717 }
718
719 let buffer = build_request_envelope(
720 context,
721 recv_registered.connection_info.clone(),
722 send_registered.as_ref().map(|r| r.connection_info.clone()),
723 request,
724 payload_codec,
725 )?;
726 REQUEST_PLANE_QUEUE_SECONDS.observe(queue_start.elapsed().as_secs_f64());
727
728 let tx_start = Instant::now();
729 let request_plane_response = self.dispatch_buffer(address, buffer, context.id()).await?;
730 REQUEST_PLANE_SEND_SECONDS.observe(tx_start.elapsed().as_secs_f64());
731
732 if let Some(err) = detect_worker_rejection_response(&request_plane_response) {
737 tracing::warn!(
738 request_id = context.id(),
739 worker_response = %err.to_string(),
740 "Request rejected by worker"
741 );
742 return Err(err.into());
743 }
744
745 if let Some(stream) = input_stream {
751 let request_stream_provider = send_registered.map(|r| {
752 let (_conn_info, provider) = r.into_parts();
753 provider
754 });
755 spawn_request_stream_forwarder(
756 request_stream_provider,
757 stream,
758 engine_ctx.clone(),
759 payload_codec,
760 )
761 .await?;
762 }
763
764 let _nvtx_wait = dynamo_nvtx_range!("transport.tcp.wait_backend");
765 tracing::trace!(request_id = context.id(), "awaiting transport handshake");
766
767 let (_recv_conn_info, response_stream_provider) = recv_registered.into_parts();
769
770 let response_stream = match response_stream_provider.await {
773 Ok(Ok(stream)) => stream,
774 Ok(Err(e)) => {
775 return Err(anyhow::anyhow!(pre_stream_failure_error(e)));
776 }
777 Err(_recv_err) => {
778 return Err(anyhow::anyhow!(
781 DynamoError::builder()
782 .error_type(ErrorType::Disconnected)
783 .message("Worker disconnected before response stream was established")
784 .build()
785 ));
786 }
787 };
788 drop(_nvtx_wait);
789
790 Ok(decode_response_stream(
791 response_stream.rx,
792 engine_ctx,
793 queue_start,
794 tx_start,
795 inflight_guard,
796 payload_codec,
797 ))
798 }
799
800 async fn register_streams(
807 &self,
808 engine_ctx: Arc<dyn crate::engine::AsyncEngineContext>,
809 enable_request_stream: bool,
810 enable_response_stream: bool,
811 ) -> Result<
812 (
813 Option<RegisteredStream<StreamSender>>,
814 Option<RegisteredStream<StreamReceiver>>,
815 ),
816 Error,
817 > {
818 let options = StreamOptions::builder()
819 .context(engine_ctx)
820 .enable_request_stream(enable_request_stream)
821 .enable_response_stream(enable_response_stream)
822 .build()?;
823
824 let pending: PendingConnections = self.resp_transport.register(options).await;
825 let (send_stream, recv_stream) = pending.into_parts();
826
827 debug_assert_eq!(
831 send_stream.is_some(),
832 enable_request_stream,
833 "data-plane registration: request-stream presence does not match request"
834 );
835 debug_assert_eq!(
836 recv_stream.is_some(),
837 enable_response_stream,
838 "data-plane registration: response-stream presence does not match request"
839 );
840
841 Ok((send_stream, recv_stream))
842 }
843
844 async fn dispatch_buffer(
852 &self,
853 address: String,
854 buffer: bytes::Bytes,
855 request_id: &str,
856 ) -> Result<bytes::Bytes, Error> {
857 let mut headers = std::collections::HashMap::new();
858 inject_trace_headers_into_map(&mut headers);
859 headers.insert("request-id".to_string(), request_id.to_string());
860 let send_ts_ns = std::time::SystemTime::now()
861 .duration_since(std::time::UNIX_EPOCH)
862 .unwrap_or_default()
863 .as_nanos() as u64;
864 headers.insert("x-frontend-send-ts-ns".to_string(), send_ts_ns.to_string());
865
866 let _nvtx_send = dynamo_nvtx_range!("transport.tcp.send");
867 let ack = self
868 .req_client
869 .send_request(address, buffer, headers)
870 .await?;
871 drop(_nvtx_send);
872 Ok(ack)
873 }
874}
875
876fn detect_worker_rejection_response(res_bytes: &[u8]) -> Option<DynamoError> {
879 const OVERLOAD_PREFIX: &[u8] = b"Server overloaded:";
880 const UNAVAILABLE_PREFIX: &[u8] = b"Server unavailable:";
881
882 let error_type = if res_bytes.starts_with(OVERLOAD_PREFIX) {
883 ErrorType::WorkerOverloaded
887 } else if res_bytes.starts_with(UNAVAILABLE_PREFIX) {
888 ErrorType::Unavailable
889 } else {
890 return None;
891 };
892
893 let msg = String::from_utf8_lossy(res_bytes).into_owned();
894 Some(
895 DynamoError::builder()
896 .error_type(error_type)
897 .message(msg)
898 .build(),
899 )
900}
901
902#[cfg(test)]
903mod rejection_detection_tests {
904 use super::*;
905
906 #[test]
907 fn overload_payload_maps_to_worker_overloaded() {
908 let err = detect_worker_rejection_response(b"Server overloaded: worker at capacity")
909 .expect("should detect overload");
910 assert_eq!(err.error_type(), ErrorType::WorkerOverloaded);
911 }
912
913 #[test]
914 fn empty_ack_is_not_overload() {
915 assert!(detect_worker_rejection_response(b"").is_none());
917 assert!(detect_worker_rejection_response(br#"{"data":"chunk"}"#).is_none());
918 }
919
920 #[test]
921 fn detected_overload_preserves_worker_scope() {
922 let err =
923 detect_worker_rejection_response(b"Server overloaded: test").expect("should detect");
924 let any_err: anyhow::Error = err.into();
925 assert!(crate::error::match_error_chain(
926 any_err.as_ref(),
927 &[ErrorType::WorkerOverloaded],
928 &[]
929 ));
930 }
931}
932
933#[async_trait::async_trait]
934impl<T, U> AsyncEngine<SingleIn<AddressedRequest<T>>, ManyOut<U>, Error> for AddressedPushRouter
935where
936 T: Data + Serialize,
937 U: Data + for<'de> Deserialize<'de> + MaybeError,
938{
939 async fn generate(&self, request: SingleIn<AddressedRequest<T>>) -> Result<ManyOut<U>, Error> {
940 let (addressed_request, context) = request.transfer(());
941 let (request, address, instance_info) = addressed_request.into_parts();
942
943 let first_response_guard = context
944 .get_optional::<FirstResponseGuard>(FIRST_RESPONSE_GUARD_CONTEXT_KEY)
945 .map_err(Error::msg)?;
946
947 if let Some(guard) = first_response_guard.and_then(|guard| guard.take()) {
948 let permit =
949 try_acquire_retained_dispatch_permit(&RETAINED_FIRST_RESPONSE_DISPATCH_PERMITS)?;
950 let router = self.clone();
951 let dispatch = async move {
952 router
953 .dispatch_and_finalize::<T, U>(
954 &context,
955 address,
956 instance_info.as_ref(),
957 Some(&request),
958 None,
959 )
960 .await
961 };
962 return dispatch_with_first_response_guard(dispatch, guard, permit).await;
963 }
964
965 self.dispatch_and_finalize::<T, U>(
966 &context,
967 address,
968 instance_info.as_ref(),
969 Some(&request),
970 None,
971 )
972 .await
973 }
974}
975
976#[async_trait::async_trait]
993pub trait StreamingDispatch<T, U>: Send + Sync
994where
995 T: Data + Serialize,
996 U: Data + for<'de> Deserialize<'de> + MaybeError,
997{
998 async fn generate(&self, request: SingleIn<AddressedRequest<T>>) -> Result<ManyOut<U>, Error>;
1000
1001 async fn generate_bidirectional(
1003 &self,
1004 instance: Instance,
1005 address: String,
1006 input: ManyIn<T>,
1007 ) -> Result<ManyOut<U>, Error>;
1008
1009 async fn on_instance_removed(&self, _id: &EndpointInstanceId) {}
1012
1013 async fn on_instance_added(&self, _id: &EndpointInstanceId) {}
1016}
1017
1018#[async_trait::async_trait]
1019impl<T, U> StreamingDispatch<T, U> for AddressedPushRouter
1020where
1021 T: Data + Serialize,
1022 U: Data + for<'de> Deserialize<'de> + MaybeError,
1023{
1024 async fn generate(&self, request: SingleIn<AddressedRequest<T>>) -> Result<ManyOut<U>, Error> {
1025 <Self as AsyncEngine<SingleIn<AddressedRequest<T>>, ManyOut<U>, Error>>::generate(
1028 self, request,
1029 )
1030 .await
1031 }
1032
1033 async fn generate_bidirectional(
1034 &self,
1035 instance: Instance,
1036 address: String,
1037 input: ManyIn<T>,
1038 ) -> Result<ManyOut<U>, Error> {
1039 self.dispatch_bidirectional(instance, address, input).await
1040 }
1041
1042 async fn on_instance_removed(&self, id: &EndpointInstanceId) {
1043 let n = self.cancel_instance_streams(id).await;
1044 if n > 0 {
1045 tracing::warn!(
1046 namespace = %id.namespace,
1047 component = %id.component,
1048 endpoint = %id.endpoint,
1049 instance_id = id.instance_id,
1050 cancelled = n,
1051 "Cancelled pending response streams for removed instance (discovery-driven cleanup)"
1052 );
1053 }
1054 }
1055
1056 async fn on_instance_added(&self, id: &EndpointInstanceId) {
1057 self.clear_instance_tombstone(id).await;
1058 }
1059}
1060
1061#[cfg(test)]
1062mod tests {
1063 use super::{
1064 CONTROL_MESSAGE_MAX_BYTES, ConnectionInfo, FIRST_RESPONSE_GUARD_CONTEXT_KEY,
1065 FirstResponseGuard, RequestControlMessage, RequestPlanePayloadCodec, RequestType,
1066 ResponseType, TwoPartCodec, attach_first_response_guard, build_request_envelope,
1067 dispatch_with_first_response_guard, payload_codec_for_worker,
1068 propagate_first_response_guard, serialize_control_message,
1069 try_acquire_retained_dispatch_permit,
1070 };
1071 use crate::{
1072 component::{Instance, TransportType},
1073 error::{ErrorType, match_error_chain},
1074 pipeline::{AsyncEngineContextProvider, Context, ManyOut, ResponseStream},
1075 protocols::annotated::Annotated,
1076 };
1077 use serde::{Deserialize, Serialize};
1078 use std::{collections::BTreeMap, sync::Arc, time::Duration};
1079 use tokio::sync::{Semaphore, oneshot, oneshot::error::TryRecvError};
1080 use tokio_stream::{StreamExt, wrappers::ReceiverStream};
1081
1082 struct DropSignal(Option<oneshot::Sender<()>>);
1083
1084 impl Drop for DropSignal {
1085 fn drop(&mut self) {
1086 if let Some(sender) = self.0.take() {
1087 let _ = sender.send(());
1088 }
1089 }
1090 }
1091
1092 fn base_control_message(metadata: BTreeMap<String, String>) -> RequestControlMessage {
1093 RequestControlMessage {
1094 id: "request-123".to_string(),
1095 request_type: RequestType::SingleIn,
1096 response_type: ResponseType::ManyOut,
1097 payload_codec: RequestPlanePayloadCodec::Json,
1098 connection_info: ConnectionInfo {
1099 transport: "tcp".to_string(),
1100 info: "{}".to_string(),
1101 },
1102 metadata,
1103 frontend_send_ts_ns: None,
1104 request_stream_connection_info: None,
1105 }
1106 }
1107
1108 #[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
1109 struct TestRequest {
1110 value: u64,
1111 }
1112
1113 #[test]
1114 fn legacy_worker_without_codec_metadata_receives_json() {
1115 let worker = Instance {
1116 component: "worker".to_string(),
1117 endpoint: "generate".to_string(),
1118 namespace: "default".to_string(),
1119 instance_id: 42,
1120 transport: TransportType::Nats("worker.generate".to_string()),
1121 device_type: None,
1122 request_plane_codec: None,
1123 };
1124 let payload_codec = payload_codec_for_worker(Some(&worker));
1125 assert_eq!(payload_codec, RequestPlanePayloadCodec::Json);
1126
1127 let request = TestRequest { value: 123 };
1128 let buffer = build_request_envelope(
1129 &Context::new(()),
1130 ConnectionInfo {
1131 transport: "tcp".to_string(),
1132 info: "{}".to_string(),
1133 },
1134 None,
1135 Some(&request),
1136 payload_codec,
1137 )
1138 .expect("legacy-worker request envelope should encode");
1139 let message = TwoPartCodec::default()
1140 .decode_message(buffer)
1141 .expect("request envelope should decode");
1142
1143 let control: RequestControlMessage = serde_json::from_slice(&message.header).unwrap();
1144 assert_eq!(control.payload_codec, RequestPlanePayloadCodec::Json);
1145 assert_eq!(
1146 serde_json::from_slice::<TestRequest>(&message.data).unwrap(),
1147 request
1148 );
1149 }
1150
1151 #[test]
1152 fn serialize_control_message_succeeds_under_limit() {
1153 let mut metadata = BTreeMap::new();
1154 metadata.insert("x-tiny-blob".to_string(), "alpha".to_string());
1155
1156 let ctrl = serialize_control_message(&base_control_message(metadata))
1157 .expect("control message should serialize under the limit");
1158 assert!(ctrl.len() <= CONTROL_MESSAGE_MAX_BYTES);
1159 }
1160
1161 #[test]
1162 fn serialize_control_message_errors_over_limit() {
1163 let mut metadata = BTreeMap::new();
1164 metadata.insert(
1165 "x-large-blob".to_string(),
1166 "x".repeat(CONTROL_MESSAGE_MAX_BYTES),
1167 );
1168
1169 let err = serialize_control_message(&base_control_message(metadata))
1170 .expect_err("oversized control message should fail")
1171 .to_string();
1172 assert!(err.contains("request control message too large"));
1173 assert!(err.contains(&CONTROL_MESSAGE_MAX_BYTES.to_string()));
1174 }
1175
1176 #[test]
1177 fn propagated_first_response_guard_is_taken_once() {
1178 let (guard_dropped_tx, mut guard_dropped_rx) = oneshot::channel();
1179 let mut source_context = Context::new(());
1180 attach_first_response_guard(
1181 &mut source_context,
1182 Arc::new(DropSignal(Some(guard_dropped_tx))),
1183 );
1184 let mut derived_context = Context::new(());
1185 propagate_first_response_guard(&source_context, &mut derived_context).unwrap();
1186
1187 let source_guard = source_context
1188 .get::<FirstResponseGuard>(FIRST_RESPONSE_GUARD_CONTEXT_KEY)
1189 .unwrap();
1190 let derived_guard = derived_context
1191 .get::<FirstResponseGuard>(FIRST_RESPONSE_GUARD_CONTEXT_KEY)
1192 .unwrap();
1193 let retained = derived_guard.take().expect("derived context should win");
1194 assert!(source_guard.take().is_none());
1195
1196 drop(retained);
1197 assert_eq!(guard_dropped_rx.try_recv(), Ok(()));
1198 }
1199
1200 #[tokio::test]
1201 async fn first_response_guard_outlives_cancelled_dispatch_waiter() {
1202 let (raw_tx, raw_rx) = tokio::sync::mpsc::channel(1);
1203 let response_context = Context::new(()).context();
1204 let (dispatch_started_tx, dispatch_started_rx) = oneshot::channel();
1205 let (release_dispatch_tx, release_dispatch_rx) = oneshot::channel();
1206 let (guard_dropped_tx, mut guard_dropped_rx) = oneshot::channel();
1207 let mut source_context = Context::new(());
1208 attach_first_response_guard(
1209 &mut source_context,
1210 Arc::new(DropSignal(Some(guard_dropped_tx))),
1211 );
1212 let mut derived_context = Context::new(());
1213 propagate_first_response_guard(&source_context, &mut derived_context).unwrap();
1214 let guard = derived_context
1215 .get::<FirstResponseGuard>(FIRST_RESPONSE_GUARD_CONTEXT_KEY)
1216 .unwrap()
1217 .take()
1218 .unwrap();
1219 drop(source_context);
1220 drop(derived_context);
1221 let permits = Arc::new(Semaphore::new(1));
1222 let permit = try_acquire_retained_dispatch_permit(&permits).unwrap();
1223
1224 let waiter = tokio::spawn(dispatch_with_first_response_guard(
1225 async move {
1226 let _ = dispatch_started_tx.send(());
1227 let _ = release_dispatch_rx.await;
1228 let response: ManyOut<Annotated<u64>> =
1229 ResponseStream::new(Box::pin(ReceiverStream::new(raw_rx)), response_context);
1230 Ok(response)
1231 },
1232 guard,
1233 permit,
1234 ));
1235
1236 dispatch_started_rx.await.unwrap();
1237 waiter.abort();
1238 let _ = waiter.await;
1239 assert_eq!(guard_dropped_rx.try_recv(), Err(TryRecvError::Empty));
1240 let error = try_acquire_retained_dispatch_permit(&permits).unwrap_err();
1241 assert!(match_error_chain(
1242 error.as_ref(),
1243 &[ErrorType::ResourceExhausted],
1244 &[],
1245 ));
1246
1247 release_dispatch_tx.send(()).unwrap();
1248 raw_tx.send(Annotated::from_data(1_u64)).await.unwrap();
1249 tokio::time::timeout(Duration::from_secs(1), guard_dropped_rx)
1250 .await
1251 .expect("source guard was not released after the first worker response")
1252 .unwrap();
1253 let released_permit = tokio::time::timeout(Duration::from_secs(1), async {
1254 loop {
1255 if let Ok(permit) = try_acquire_retained_dispatch_permit(&permits) {
1256 break permit;
1257 }
1258 tokio::task::yield_now().await;
1259 }
1260 })
1261 .await
1262 .expect("retained dispatch permit was not released");
1263 drop(released_permit);
1264 }
1265
1266 #[tokio::test]
1267 async fn dropping_handed_off_tail_closes_upstream() {
1268 let (raw_tx, raw_rx) = tokio::sync::mpsc::channel(1);
1269 let response_context = Context::new(()).context();
1270 let (guard_dropped_tx, guard_dropped_rx) = oneshot::channel();
1271 let permits = Arc::new(Semaphore::new(1));
1272 let permit = try_acquire_retained_dispatch_permit(&permits).unwrap();
1273 let mut response = dispatch_with_first_response_guard(
1274 async move {
1275 let response: ManyOut<Annotated<u64>> =
1276 ResponseStream::new(Box::pin(ReceiverStream::new(raw_rx)), response_context);
1277 Ok(response)
1278 },
1279 Arc::new(DropSignal(Some(guard_dropped_tx))),
1280 permit,
1281 )
1282 .await
1283 .unwrap();
1284
1285 raw_tx.send(Annotated::from_data(1_u64)).await.unwrap();
1286 assert_eq!(response.next().await.unwrap().data, Some(1));
1287 tokio::time::timeout(Duration::from_secs(1), guard_dropped_rx)
1288 .await
1289 .expect("source guard was not released after the first worker response")
1290 .unwrap();
1291
1292 drop(response);
1293 tokio::time::timeout(Duration::from_secs(1), raw_tx.closed())
1294 .await
1295 .expect("dropping the caller stream did not close the upstream tail");
1296 }
1297}