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, ErrorClass, ErrorType};
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::ResponsePlaneMode;
30use crate::pipeline::network::ResponseService;
31use crate::pipeline::network::ResponseType;
32use crate::pipeline::network::StreamOptions;
33use crate::pipeline::network::StreamPrologueError;
34use crate::pipeline::network::StreamProvider;
35use crate::pipeline::network::StreamReceiver;
36use crate::pipeline::network::StreamSender;
37use crate::pipeline::network::TwoPartCodec;
38use crate::pipeline::network::codec::TwoPartMessage;
39use crate::pipeline::network::quic_response;
40use crate::pipeline::network::tcp;
41use crate::pipeline::{ManyIn, ManyOut, PipelineError, ResponseStream, SingleIn};
42use crate::protocols::maybe_error::MaybeError;
43use crate::traits::DistributedRuntimeProvider;
44
45use anyhow::{Error, Result};
46use futures::stream::Stream;
47use parking_lot::Mutex;
48use std::pin::Pin;
49use std::task::{Context, Poll};
50use tokio::sync::{OwnedSemaphorePermit, Semaphore};
51use tokio_stream::{StreamExt, StreamNotifyClose, wrappers::ReceiverStream};
52use tracing::Instrument;
53
54pub(crate) const MIGRATION_SENSITIVE_ERROR_REASONS: &[&str] = &[
59 "request.cancelled",
60 "request.deadline_exceeded",
61 "backend.cancelled",
62 "capacity.exhausted",
63 "capacity.pool_exhausted",
64];
65
66fn is_migration_sensitive(err: &DynamoError) -> bool {
68 let mut current: Option<&(dyn std::error::Error + 'static)> = Some(err);
69 while let Some(source) = current {
70 if let Some(error) = source.downcast_ref::<DynamoError>()
71 && MIGRATION_SENSITIVE_ERROR_REASONS.contains(&error.reason().as_str())
72 {
73 return true;
74 }
75 current = source.source();
76 }
77 false
78}
79
80pub(crate) fn pre_stream_failure_error(error: StreamPrologueError) -> DynamoError {
98 let builder = DynamoError::builder()
99 .error_type(ErrorType::CannotConnect)
100 .message(format!(
101 "Worker generate() failed before response stream: {error}"
102 ));
103
104 match error.typed_error {
105 Some(typed)
106 if !is_migration_sensitive(&typed) && typed.class() == ErrorClass::InvalidRequest =>
107 {
108 typed
109 }
110 Some(typed) if !is_migration_sensitive(&typed) => builder.cause(typed).build(),
111 _ => builder.build(),
112 }
113}
114
115#[cfg(any(test, feature = "testing"))]
118#[doc(hidden)]
119pub mod testing {
120 use super::{DynamoError, StreamPrologueError};
121
122 pub fn migration_sensitive_error_reasons() -> &'static [&'static str] {
124 super::MIGRATION_SENSITIVE_ERROR_REASONS
125 }
126
127 pub fn pre_stream_failure_error(error: StreamPrologueError) -> DynamoError {
128 super::pre_stream_failure_error(error)
129 }
130}
131
132const FIRST_RESPONSE_GUARD_CONTEXT_KEY: &str = "dynamo.request_plane.first_response_guard";
133const MAX_RETAINED_FIRST_RESPONSE_DISPATCHES: usize = 1024;
136static RETAINED_FIRST_RESPONSE_DISPATCH_PERMITS: LazyLock<Arc<Semaphore>> =
137 LazyLock::new(|| Arc::new(Semaphore::new(MAX_RETAINED_FIRST_RESPONSE_DISPATCHES)));
138
139#[derive(Clone)]
140struct FirstResponseGuard {
141 guard: Arc<Mutex<Option<EngineContextGuard>>>,
142}
143
144impl FirstResponseGuard {
145 fn new(guard: EngineContextGuard) -> Self {
146 Self {
147 guard: Arc::new(Mutex::new(Some(guard))),
148 }
149 }
150
151 fn take(&self) -> Option<EngineContextGuard> {
152 self.guard.lock().take()
153 }
154}
155
156pub fn attach_first_response_guard<T: Data>(
159 context: &mut context::Context<T>,
160 guard: EngineContextGuard,
161) {
162 context.insert(
163 FIRST_RESPONSE_GUARD_CONTEXT_KEY,
164 FirstResponseGuard::new(guard),
165 );
166}
167
168pub fn propagate_first_response_guard<S: Data, T: Data>(
170 source: &context::Context<S>,
171 target: &mut context::Context<T>,
172) -> Result<(), Error> {
173 if let Some(guard) = source
174 .get_optional::<FirstResponseGuard>(FIRST_RESPONSE_GUARD_CONTEXT_KEY)
175 .map_err(Error::msg)?
176 {
177 target.insert(FIRST_RESPONSE_GUARD_CONTEXT_KEY, guard.as_ref().clone());
178 }
179 Ok(())
180}
181
182fn try_acquire_retained_dispatch_permit(
183 permits: &Arc<Semaphore>,
184) -> Result<OwnedSemaphorePermit, Error> {
185 permits.clone().try_acquire_owned().map_err(|_| {
186 DynamoError::builder()
187 .error_type(ErrorType::ResourceExhausted)
188 .message("retained request dispatch limit reached")
189 .build()
190 .into()
191 })
192}
193
194async fn dispatch_with_first_response_guard<F, U>(
197 dispatch: F,
198 guard: EngineContextGuard,
199 permit: OwnedSemaphorePermit,
200) -> Result<ManyOut<U>, Error>
201where
202 F: Future<Output = Result<ManyOut<U>, Error>> + Send + 'static,
203 U: Data + MaybeError,
204{
205 let (dispatch_tx, dispatch_rx) = tokio::sync::oneshot::channel();
206
207 tokio::spawn(
208 async move {
209 let mut response = match dispatch.await {
210 Ok(response) => response,
211 Err(error) => {
212 let _ = dispatch_tx.send(Err(error));
213 return;
214 }
215 };
216
217 let response_context = response.context();
218 let (first_tx, first_rx) = tokio::sync::oneshot::channel::<(Option<U>, ManyOut<U>)>();
219 let stream = async_stream::stream! {
220 match first_rx.await {
221 Ok((first, mut tail)) => {
222 if let Some(first) = first {
223 yield first;
224 }
225 while let Some(item) = tail.next().await {
226 yield item;
227 }
228 }
229 Err(_) => {
230 yield U::from_err(DynamoError::msg(
231 "retained request dispatch ended before first response handoff",
232 ));
233 }
234 }
235 };
236 let handoff: ManyOut<U> = ResponseStream::new(Box::pin(stream), response_context);
237 let _ = dispatch_tx.send(Ok(handoff));
238
239 let first = response.next().await;
240 drop(guard);
241 drop(permit);
242 let _ = first_tx.send((first, response));
243 }
244 .in_current_span(),
245 );
246
247 dispatch_rx
248 .await
249 .map_err(|_| anyhow::anyhow!("retained request dispatch ended before setup completed"))?
250}
251
252fn decode_response_stream<U>(
258 response_rx: tokio::sync::mpsc::Receiver<bytes::Bytes>,
259 engine_ctx: Arc<dyn crate::engine::AsyncEngineContext>,
260 queue_start: Instant,
261 tx_start: Instant,
262 inflight_guard: InflightGuard,
263 payload_codec: RequestPlanePayloadCodec,
264) -> ManyOut<U>
265where
266 U: Data + for<'de> Deserialize<'de> + MaybeError,
267{
268 let engine_ctx_for_stream = engine_ctx.clone();
269 let mut is_complete_final = false;
270 let mut first_response = true;
271 let stream = StreamNotifyClose::new(ReceiverStream::new(response_rx)).filter_map(move |res| {
272 if let Some(res_bytes) = res {
273 if first_response {
274 first_response = false;
275 REQUEST_PLANE_ROUNDTRIP_TTFT_SECONDS.observe(tx_start.elapsed().as_secs_f64());
276 STAGE_DURATION_SECONDS
277 .with_label_values(&["transport_roundtrip"])
278 .observe(queue_start.elapsed().as_secs_f64());
279 }
280 if is_complete_final {
281 let err = DynamoError::msg(
282 "Response received after generation ended - this should never happen",
283 );
284 return Some(U::from_err(err));
285 }
286 match payload_codec.decode::<NetworkStreamWrapper<U>>(&res_bytes) {
287 Ok(item) => {
288 is_complete_final = item.complete_final;
289 if let Some(data) = item.data {
290 Some(data)
291 } else if is_complete_final {
292 None
293 } else {
294 let err =
295 DynamoError::msg("Empty response received - this should never happen");
296 Some(U::from_err(err))
297 }
298 }
299 Err(err) => {
300 let response_bytes_len = res_bytes.len();
301 tracing::warn!(
302 %err,
303 codec = payload_codec.name(),
304 response_bytes_len,
305 "failed deserializing request-plane response"
306 );
307 Some(U::from_err(DynamoError::msg(err.to_string())))
308 }
309 }
310 } else if is_complete_final {
311 None
312 } else if engine_ctx_for_stream.is_stopped() {
313 tracing::debug!("Request cancelled and then trying to read a response");
314 None
315 } else {
316 let err = DynamoError::builder()
317 .error_type(ErrorType::Disconnected)
318 .message("Stream ended before generation completed")
319 .build();
320 tracing::debug!("{err}");
321 Some(U::from_err(err))
322 }
323 });
324
325 inflight_guard.disarm();
326 let stream = InflightDecStream { inner: stream };
327 ResponseStream::new(Box::pin(stream), engine_ctx)
328}
329
330const CONTROL_MESSAGE_MAX_BYTES: usize = 128 * 1024;
331
332fn serialize_control_message(control_message: &RequestControlMessage) -> Result<Vec<u8>, Error> {
333 let ctrl = serde_json::to_vec(control_message)?;
334 if ctrl.len() > CONTROL_MESSAGE_MAX_BYTES {
335 return Err(PipelineError::Generic(format!(
336 "request control message too large: {} bytes exceeds limit {}",
337 ctrl.len(),
338 CONTROL_MESSAGE_MAX_BYTES
339 ))
340 .into());
341 }
342 Ok(ctrl)
343}
344
345fn build_request_envelope<T>(
352 context: &context::Context<()>,
353 recv_conn_info: ConnectionInfo,
354 send_conn_info: Option<ConnectionInfo>,
355 request: Option<&T>,
356 payload_codec: RequestPlanePayloadCodec,
357) -> Result<bytes::Bytes, Error>
358where
359 T: serde::Serialize,
360{
361 let request_id = context.id();
362 let request_type = if send_conn_info.is_some() {
363 RequestType::ManyIn
364 } else {
365 RequestType::SingleIn
366 };
367 let control_message = RequestControlMessage {
368 id: request_id.to_string(),
369 request_type,
370 response_type: ResponseType::ManyOut,
371 payload_codec,
372 connection_info: recv_conn_info,
373 metadata: context.metadata().clone(),
374 frontend_send_ts_ns: None,
375 request_stream_connection_info: send_conn_info,
376 };
377
378 let ctrl = serialize_control_message(&control_message)?;
379 let data: Option<Vec<u8>> = match request {
380 Some(req) => Some(payload_codec.encode(req)?),
381 None => None,
382 };
383
384 let msg = match data {
385 Some(d) => {
386 tracing::trace!(
387 request_id,
388 "packaging two-part message; ctrl: {} bytes, data: {} bytes",
389 ctrl.len(),
390 d.len(),
391 );
392 TwoPartMessage::from_parts(ctrl.into(), d.into())
393 }
394 None => {
395 tracing::trace!(
396 request_id,
397 "packaging bidirectional header-only envelope; ctrl: {} bytes",
398 ctrl.len(),
399 );
400 TwoPartMessage::from_header(ctrl.into())
401 }
402 };
403
404 let codec = TwoPartCodec::default();
405 let buffer = codec.encode_message(msg)?;
406 Ok(buffer)
407}
408
409fn payload_codec_for_worker(instance: Option<&Instance>) -> RequestPlanePayloadCodec {
410 instance
411 .and_then(|instance| instance.request_plane_codec)
412 .unwrap_or(RequestPlanePayloadCodec::Json)
413}
414
415async fn spawn_request_stream_forwarder<T>(
420 request_stream_provider: Option<StreamProvider<StreamSender>>,
421 mut input_stream: crate::engine::DataStream<T>,
422 engine_ctx: Arc<dyn crate::engine::AsyncEngineContext>,
423 payload_codec: RequestPlanePayloadCodec,
424) -> Result<(), Error>
425where
426 T: serde::Serialize + Send + 'static,
427{
428 let Some(provider) = request_stream_provider else {
429 return Ok(());
430 };
431
432 let request_sender = match provider.await {
433 Ok(Ok(sender)) => sender,
434 Ok(Err(e)) => {
435 return Err(anyhow::anyhow!(
436 DynamoError::builder()
437 .error_type(ErrorType::CannotConnect)
438 .message(format!("Worker dial-in failed for request stream: {e}"))
439 .build()
440 ));
441 }
442 Err(_) => {
443 return Err(anyhow::anyhow!(
444 DynamoError::builder()
445 .error_type(ErrorType::Disconnected)
446 .message("Worker disconnected before request stream was established")
447 .build()
448 ));
449 }
450 };
451
452 tokio::spawn(async move {
458 loop {
459 let item = tokio::select! {
460 biased;
461 _ = engine_ctx.killed() => break,
462 _ = engine_ctx.stopped() => break,
463 item = input_stream.next() => match item {
464 Some(item) => item,
465 None => break,
466 },
467 };
468 let bytes = match payload_codec.encode(&item) {
469 Ok(b) => b,
470 Err(e) => {
471 tracing::error!(
476 error = %e,
477 codec = payload_codec.name(),
478 "failed to serialize bidirectional request frame; killing context"
479 );
480 engine_ctx.kill();
481 break;
482 }
483 };
484 if request_sender.send(bytes.into()).await.is_err() {
485 tracing::debug!("worker request-stream receiver dropped; forwarder exiting");
486 break;
487 }
488 }
489 });
490
491 Ok(())
492}
493
494struct InflightGuard {
498 armed: bool,
499}
500
501impl InflightGuard {
502 fn new() -> Self {
503 Self { armed: true }
504 }
505
506 fn disarm(mut self) {
509 self.armed = false;
510 }
511}
512
513impl Drop for InflightGuard {
514 fn drop(&mut self) {
515 if self.armed {
516 REQUEST_PLANE_INFLIGHT.dec();
517 }
518 }
519}
520
521struct InflightDecStream<S> {
523 inner: S,
524}
525
526impl<S, T> Stream for InflightDecStream<S>
527where
528 S: Stream<Item = T> + Unpin,
529{
530 type Item = T;
531
532 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
533 Pin::new(&mut self.inner).poll_next(cx)
534 }
535}
536
537impl<S> Drop for InflightDecStream<S> {
538 fn drop(&mut self) {
539 REQUEST_PLANE_INFLIGHT.dec();
540 }
541}
542
543fn tcp_subject_of(conn_info: &ConnectionInfo) -> Option<String> {
544 serde_json::from_str::<tcp::TcpStreamConnectionInfo>(&conn_info.info)
545 .ok()
546 .map(|ci| ci.subject)
547}
548
549pub struct AddressedRequest<T> {
550 request: T,
551 address: String,
552 instance: Option<Instance>,
555}
556
557impl<T> AddressedRequest<T> {
558 pub fn new(request: T, address: String) -> Self {
559 Self {
560 request,
561 address,
562 instance: None,
563 }
564 }
565
566 pub fn with_instance(request: T, address: String, instance: Instance) -> Self {
567 Self {
568 request,
569 address,
570 instance: Some(instance),
571 }
572 }
573
574 pub fn for_instance(request: T, instance: Instance) -> Self {
575 let address = instance.transport.address().to_string();
576 Self::with_instance(request, address, instance)
577 }
578
579 pub fn into_parts(self) -> (T, String, Option<Instance>) {
582 (self.request, self.address, self.instance)
583 }
584}
585
586#[derive(Clone)]
587pub struct AddressedPushRouter {
588 req_client: Arc<dyn RequestPlaneClient>,
590
591 request_callbacks: Arc<tcp::server::TcpStreamServer>,
592 responses: ResponseServer,
593}
594
595#[derive(Clone)]
596enum ResponseServer {
597 Tcp(Arc<tcp::server::TcpStreamServer>),
598 Quic(Arc<quic_response::QuicResponseServer>),
599}
600
601impl AddressedPushRouter {
602 pub fn new(
607 req_client: Arc<dyn RequestPlaneClient>,
608 responses: Arc<tcp::server::TcpStreamServer>,
609 ) -> Result<Arc<Self>> {
610 Ok(Arc::new(Self {
611 req_client,
612 request_callbacks: responses.clone(),
613 responses: ResponseServer::Tcp(responses),
614 }))
615 }
616
617 fn new_quic(
618 req_client: Arc<dyn RequestPlaneClient>,
619 request_callbacks: Arc<tcp::server::TcpStreamServer>,
620 responses: Arc<quic_response::QuicResponseServer>,
621 ) -> Arc<Self> {
622 Arc::new(Self {
623 req_client,
624 request_callbacks,
625 responses: ResponseServer::Quic(responses),
626 })
627 }
628
629 pub async fn from_runtime_provider(
630 provider: &impl DistributedRuntimeProvider,
631 ) -> Result<Arc<Self>> {
632 let manager = provider.drt().network_manager();
633 let req_client = manager.create_client()?;
634 let request_callbacks = provider.drt().tcp_server().await?;
635
636 tracing::debug!(
637 transport = req_client.transport_name(),
638 "Creating AddressedPushRouter with request plane client"
639 );
640
641 match provider.drt().response_plane() {
642 ResponsePlaneMode::Tcp => Self::new(req_client, request_callbacks),
643 ResponsePlaneMode::Quic => {
644 let responses = provider.drt().quic_response_server().await?;
645 Ok(Self::new_quic(req_client, request_callbacks, responses))
646 }
647 }
648 }
649
650 pub async fn cancel_instance_streams(&self, instance_id: &EndpointInstanceId) -> usize {
654 match &self.responses {
655 ResponseServer::Tcp(responses) => responses.cancel_instance_streams(instance_id).await,
656 ResponseServer::Quic(responses) => {
657 let response_count = responses.cancel_instance_streams(instance_id).await;
658 response_count
659 + self
660 .request_callbacks
661 .cancel_instance_streams(instance_id)
662 .await
663 }
664 }
665 }
666
667 pub async fn clear_instance_tombstone(&self, instance_id: &EndpointInstanceId) {
669 match &self.responses {
670 ResponseServer::Tcp(responses) => {
671 responses.clear_instance_tombstone(instance_id).await;
672 }
673 ResponseServer::Quic(responses) => {
674 responses.clear_instance_tombstone(instance_id).await;
675 self.request_callbacks
676 .clear_instance_tombstone(instance_id)
677 .await;
678 }
679 }
680 }
681
682 pub async fn dispatch_bidirectional<T, U>(
688 &self,
689 instance: Instance,
690 address: String,
691 input: ManyIn<T>,
692 ) -> Result<ManyOut<U>, Error>
693 where
694 T: Data + Serialize,
695 U: Data + for<'de> Deserialize<'de> + MaybeError,
696 {
697 let (request_stream, context) = input.into_parts();
698 let input_stream = request_stream.take().ok_or_else(|| {
699 anyhow::anyhow!("RequestStream::take called twice on bidirectional dispatch input")
700 })?;
701
702 self.dispatch_and_finalize::<T, U>(
703 &context,
704 address,
705 Some(&instance),
706 None,
707 Some(input_stream),
708 false,
709 )
710 .await
711 }
712
713 async fn dispatch_and_finalize<T, U>(
721 &self,
722 context: &context::Context<()>,
723 address: String,
724 instance: Option<&Instance>,
725 request: Option<&T>,
726 input_stream: Option<crate::engine::DataStream<T>>,
727 defer_cancellation_until_prologue: bool,
728 ) -> Result<ManyOut<U>, Error>
729 where
730 T: Data + Serialize,
731 U: Data + for<'de> Deserialize<'de> + MaybeError,
732 {
733 let engine_ctx = context.context();
734
735 let queue_start = Instant::now();
736 REQUEST_PLANE_INFLIGHT.inc();
737 let inflight_guard = InflightGuard::new();
738
739 let enable_request_stream = input_stream.is_some();
740 let payload_codec = payload_codec_for_worker(instance);
741
742 let (send_registered, recv_registered) = self
746 .register_streams(
747 engine_ctx.clone(),
748 enable_request_stream,
749 defer_cancellation_until_prologue,
750 )
751 .await?;
752
753 let response_registration = recv_registered.registration_id();
757 let response_subject = tcp_subject_of(&recv_registered.connection_info);
758 let send_subject = send_registered
759 .as_ref()
760 .and_then(|r| tcp_subject_of(&r.connection_info));
761 if let Some(inst) = instance {
762 let instance_id = inst.endpoint_instance_id();
763 let associated = match &self.responses {
764 ResponseServer::Tcp(responses) => match response_subject.as_deref() {
765 Some(subject) => {
766 responses
767 .associate_instance(subject, send_subject.as_deref(), &instance_id)
768 .await
769 }
770 None => false,
771 },
772 ResponseServer::Quic(responses) => {
773 let response_ok = match response_registration {
774 Some(registration_id) => {
775 responses
776 .associate_instance(registration_id, &instance_id)
777 .await
778 }
779 None => false,
780 };
781 let request_ok = match send_subject.as_deref() {
782 Some(subject) => {
783 self.request_callbacks
784 .associate_request_instance(subject, &instance_id)
785 .await
786 }
787 None => true,
788 };
789 if !request_ok && let Some(registration_id) = response_registration {
790 responses.cancel_response(registration_id).await;
791 }
792 response_ok && request_ok
793 }
794 };
795 if !associated {
796 return Err(anyhow::anyhow!(
797 DynamoError::builder()
798 .error_type(ErrorType::Disconnected)
799 .message(
800 "Worker removed before request could be sent (tombstoned instance)"
801 )
802 .build()
803 ));
804 }
805 }
806
807 let buffer = build_request_envelope(
808 context,
809 recv_registered.connection_info.clone(),
810 send_registered.as_ref().map(|r| r.connection_info.clone()),
811 request,
812 payload_codec,
813 )?;
814 REQUEST_PLANE_QUEUE_SECONDS.observe(queue_start.elapsed().as_secs_f64());
815
816 let tx_start = Instant::now();
817 let request_plane_response = self.dispatch_buffer(address, buffer, context.id()).await?;
818 REQUEST_PLANE_SEND_SECONDS.observe(tx_start.elapsed().as_secs_f64());
819
820 if let Some(err) = detect_worker_rejection_response(&request_plane_response) {
825 tracing::warn!(
826 request_id = context.id(),
827 worker_response = %err.to_string(),
828 "Request rejected by worker"
829 );
830 return Err(err.into());
831 }
832
833 if let Some(stream) = input_stream {
839 let request_stream_provider = send_registered.map(|r| {
840 let (_conn_info, provider) = r.into_parts();
841 provider
842 });
843 spawn_request_stream_forwarder(
844 request_stream_provider,
845 stream,
846 engine_ctx.clone(),
847 payload_codec,
848 )
849 .await?;
850 }
851
852 let _nvtx_wait = dynamo_nvtx_range!("transport.response.wait_backend");
853 tracing::trace!(request_id = context.id(), "awaiting transport handshake");
854
855 let response_stream = match recv_registered.wait().await {
858 Ok(Ok(stream)) => stream,
859 Err(_) | Ok(Err(_)) if engine_ctx.is_stopped() || engine_ctx.is_killed() => {
860 return Ok(ResponseStream::new(
861 Box::pin(futures::stream::empty()),
862 engine_ctx,
863 ));
864 }
865 Ok(Err(e)) => {
866 return Err(anyhow::anyhow!(pre_stream_failure_error(e)));
867 }
868 Err(_recv_err) => {
869 return Err(anyhow::anyhow!(
872 DynamoError::builder()
873 .error_type(ErrorType::Disconnected)
874 .message("Worker disconnected before response stream was established")
875 .build()
876 ));
877 }
878 };
879 drop(_nvtx_wait);
880
881 Ok(decode_response_stream(
882 response_stream.rx,
883 engine_ctx,
884 queue_start,
885 tx_start,
886 inflight_guard,
887 payload_codec,
888 ))
889 }
890
891 async fn register_streams(
893 &self,
894 engine_ctx: Arc<dyn crate::engine::AsyncEngineContext>,
895 enable_request_stream: bool,
896 defer_cancellation_until_prologue: bool,
897 ) -> Result<(
898 Option<RegisteredStream<StreamSender>>,
899 RegisteredStream<StreamReceiver>,
900 )> {
901 let (send_stream, recv_stream) = match &self.responses {
902 ResponseServer::Tcp(responses) => {
903 let options = StreamOptions::builder()
904 .context(engine_ctx)
905 .enable_request_stream(enable_request_stream)
906 .enable_response_stream(true)
907 .defer_cancellation_until_prologue(defer_cancellation_until_prologue)
908 .build()?;
909 let pending: PendingConnections = responses.register(options).await;
910 pending.into_parts()
911 }
912 ResponseServer::Quic(responses) => {
913 let send_stream = if enable_request_stream {
914 let options = StreamOptions::builder()
915 .context(engine_ctx.clone())
916 .enable_request_stream(true)
917 .enable_response_stream(false)
918 .build()?;
919 let pending: PendingConnections =
920 self.request_callbacks.register(options).await;
921 pending.send_stream
922 } else {
923 None
924 };
925 (send_stream, Some(responses.register_response(engine_ctx)))
926 }
927 };
928 let recv_stream = recv_stream.ok_or_else(|| {
929 anyhow::anyhow!("response stream registration missing despite response being enabled")
930 })?;
931
932 debug_assert_eq!(
936 send_stream.is_some(),
937 enable_request_stream,
938 "data-plane registration: request-stream presence does not match request"
939 );
940 Ok((send_stream, recv_stream))
941 }
942
943 async fn dispatch_buffer(
951 &self,
952 address: String,
953 buffer: bytes::Bytes,
954 request_id: &str,
955 ) -> Result<bytes::Bytes, Error> {
956 let mut headers = std::collections::HashMap::new();
957 inject_trace_headers_into_map(&mut headers);
958 headers.insert("request-id".to_string(), request_id.to_string());
959 let send_ts_ns = std::time::SystemTime::now()
960 .duration_since(std::time::UNIX_EPOCH)
961 .unwrap_or_default()
962 .as_nanos() as u64;
963 headers.insert("x-frontend-send-ts-ns".to_string(), send_ts_ns.to_string());
964
965 let _nvtx_send = dynamo_nvtx_range!("transport.tcp.send");
966 let ack = self
967 .req_client
968 .send_request(address, buffer, headers)
969 .await?;
970 drop(_nvtx_send);
971 Ok(ack)
972 }
973}
974
975fn detect_worker_rejection_response(res_bytes: &[u8]) -> Option<DynamoError> {
978 const OVERLOAD_PREFIX: &[u8] = b"Server overloaded:";
979 let unavailable_prefix = crate::pipeline::network::ACK_UNAVAILABLE_PREFIX.as_bytes();
980
981 let error_type = if res_bytes.starts_with(OVERLOAD_PREFIX) {
982 ErrorType::WorkerOverloaded
986 } else if res_bytes.starts_with(unavailable_prefix) {
987 ErrorType::WorkerUnavailable
991 } else {
992 return None;
993 };
994
995 let msg = String::from_utf8_lossy(res_bytes).into_owned();
996 Some(
997 DynamoError::builder()
998 .error_type(error_type)
999 .message(msg)
1000 .build(),
1001 )
1002}
1003
1004#[cfg(test)]
1005mod rejection_detection_tests {
1006 use super::*;
1007
1008 #[test]
1009 fn overload_payload_maps_to_worker_overloaded() {
1010 let err = detect_worker_rejection_response(b"Server overloaded: worker at capacity")
1011 .expect("should detect overload");
1012 assert_eq!(err.error_type(), ErrorType::WorkerOverloaded);
1013 }
1014
1015 #[test]
1016 fn unavailable_payload_maps_to_worker_unavailable() {
1017 let err = detect_worker_rejection_response(b"Server unavailable: unknown endpoint x")
1018 .expect("should detect unavailable");
1019 assert_eq!(err.error_type(), ErrorType::WorkerUnavailable);
1020 }
1021
1022 #[test]
1023 fn empty_ack_is_not_overload() {
1024 assert!(detect_worker_rejection_response(b"").is_none());
1026 assert!(detect_worker_rejection_response(br#"{"data":"chunk"}"#).is_none());
1027 }
1028
1029 #[test]
1030 fn detected_overload_preserves_worker_scope() {
1031 let err =
1032 detect_worker_rejection_response(b"Server overloaded: test").expect("should detect");
1033 let any_err: anyhow::Error = err.into();
1034 assert!(crate::error::match_error_chain(
1035 any_err.as_ref(),
1036 &[ErrorType::WorkerOverloaded],
1037 &[]
1038 ));
1039 }
1040}
1041
1042#[async_trait::async_trait]
1043impl<T, U> AsyncEngine<SingleIn<AddressedRequest<T>>, ManyOut<U>, Error> for AddressedPushRouter
1044where
1045 T: Data + Serialize,
1046 U: Data + for<'de> Deserialize<'de> + MaybeError,
1047{
1048 async fn generate(&self, request: SingleIn<AddressedRequest<T>>) -> Result<ManyOut<U>, Error> {
1049 let (addressed_request, context) = request.transfer(());
1050 let (request, address, instance_info) = addressed_request.into_parts();
1051
1052 let first_response_guard = context
1053 .get_optional::<FirstResponseGuard>(FIRST_RESPONSE_GUARD_CONTEXT_KEY)
1054 .map_err(Error::msg)?;
1055
1056 if let Some(guard) = first_response_guard.and_then(|guard| guard.take()) {
1057 let permit =
1058 try_acquire_retained_dispatch_permit(&RETAINED_FIRST_RESPONSE_DISPATCH_PERMITS)?;
1059 let router = self.clone();
1060 let dispatch = async move {
1061 router
1062 .dispatch_and_finalize::<T, U>(
1063 &context,
1064 address,
1065 instance_info.as_ref(),
1066 Some(&request),
1067 None,
1068 true,
1069 )
1070 .await
1071 };
1072 return dispatch_with_first_response_guard(dispatch, guard, permit).await;
1073 }
1074
1075 self.dispatch_and_finalize::<T, U>(
1076 &context,
1077 address,
1078 instance_info.as_ref(),
1079 Some(&request),
1080 None,
1081 false,
1082 )
1083 .await
1084 }
1085}
1086
1087#[async_trait::async_trait]
1104pub trait StreamingDispatch<T, U>: Send + Sync
1105where
1106 T: Data + Serialize,
1107 U: Data + for<'de> Deserialize<'de> + MaybeError,
1108{
1109 async fn generate(&self, request: SingleIn<AddressedRequest<T>>) -> Result<ManyOut<U>, Error>;
1111
1112 async fn generate_bidirectional(
1114 &self,
1115 instance: Instance,
1116 address: String,
1117 input: ManyIn<T>,
1118 ) -> Result<ManyOut<U>, Error>;
1119
1120 async fn on_instance_removed(&self, _id: &EndpointInstanceId) {}
1122
1123 async fn on_instance_added(&self, _id: &EndpointInstanceId) {}
1126}
1127
1128#[async_trait::async_trait]
1129impl<T, U> StreamingDispatch<T, U> for AddressedPushRouter
1130where
1131 T: Data + Serialize,
1132 U: Data + for<'de> Deserialize<'de> + MaybeError,
1133{
1134 async fn generate(&self, request: SingleIn<AddressedRequest<T>>) -> Result<ManyOut<U>, Error> {
1135 <Self as AsyncEngine<SingleIn<AddressedRequest<T>>, ManyOut<U>, Error>>::generate(
1138 self, request,
1139 )
1140 .await
1141 }
1142
1143 async fn generate_bidirectional(
1144 &self,
1145 instance: Instance,
1146 address: String,
1147 input: ManyIn<T>,
1148 ) -> Result<ManyOut<U>, Error> {
1149 self.dispatch_bidirectional(instance, address, input).await
1150 }
1151
1152 async fn on_instance_removed(&self, id: &EndpointInstanceId) {
1153 let n = self.cancel_instance_streams(id).await;
1154 if n > 0 {
1155 tracing::warn!(
1156 namespace = %id.namespace,
1157 component = %id.component,
1158 endpoint = %id.endpoint,
1159 instance_id = id.instance_id,
1160 cancelled = n,
1161 "Cancelled stream registrations for removed instance (discovery-driven cleanup)"
1162 );
1163 }
1164 }
1165
1166 async fn on_instance_added(&self, id: &EndpointInstanceId) {
1167 self.clear_instance_tombstone(id).await;
1168 }
1169}
1170
1171#[cfg(test)]
1172mod tests {
1173 use super::{
1174 AddressedPushRouter, AddressedRequest, CONTROL_MESSAGE_MAX_BYTES, ConnectionInfo,
1175 FIRST_RESPONSE_GUARD_CONTEXT_KEY, FirstResponseGuard, RequestControlMessage,
1176 RequestPlaneClient, RequestPlanePayloadCodec, RequestType, ResponseType, TwoPartCodec,
1177 attach_first_response_guard, build_request_envelope, dispatch_with_first_response_guard,
1178 payload_codec_for_worker, propagate_first_response_guard, serialize_control_message,
1179 try_acquire_retained_dispatch_permit,
1180 };
1181 use crate::{
1182 component::{Instance, TransportType},
1183 engine::AsyncEngine,
1184 error::{ErrorType, match_error_chain},
1185 pipeline::{
1186 AsyncEngineContextProvider, Context, ManyOut, ResponseStream,
1187 network::{
1188 egress::unified_client::Headers,
1189 tcp::{client::TcpClient, server::TcpStreamServer},
1190 },
1191 },
1192 protocols::annotated::Annotated,
1193 };
1194 use serde::{Deserialize, Serialize};
1195 use std::{collections::BTreeMap, sync::Arc, time::Duration};
1196 use tokio::sync::{Semaphore, oneshot, oneshot::error::TryRecvError};
1197 use tokio_stream::{StreamExt, wrappers::ReceiverStream};
1198
1199 struct DropSignal(Option<oneshot::Sender<()>>);
1200
1201 impl Drop for DropSignal {
1202 fn drop(&mut self) {
1203 if let Some(sender) = self.0.take() {
1204 let _ = sender.send(());
1205 }
1206 }
1207 }
1208
1209 fn base_control_message(metadata: BTreeMap<String, String>) -> RequestControlMessage {
1210 RequestControlMessage {
1211 id: "request-123".to_string(),
1212 request_type: RequestType::SingleIn,
1213 response_type: ResponseType::ManyOut,
1214 payload_codec: RequestPlanePayloadCodec::Json,
1215 connection_info: ConnectionInfo {
1216 transport: "tcp".to_string(),
1217 info: "{}".to_string(),
1218 },
1219 metadata,
1220 frontend_send_ts_ns: None,
1221 request_stream_connection_info: None,
1222 }
1223 }
1224
1225 #[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
1226 struct TestRequest {
1227 value: u64,
1228 }
1229
1230 #[tokio::test]
1231 async fn quic_router_delivers_data_after_successful_wait() {
1232 if crate::test_utils::run_isolated(
1233 concat!(
1234 module_path!(),
1235 "::quic_router_delivers_data_after_successful_wait"
1236 ),
1237 &[],
1238 ) {
1239 return;
1240 }
1241
1242 use super::{
1243 AddressedPushRouter, AddressedRequest, RequestPlaneClient, quic_response, tcp,
1244 };
1245 use crate::pipeline::{AsyncEngine, SingleIn};
1246 use bytes::Bytes;
1247 use tokio::sync::mpsc;
1248 use tokio_util::sync::CancellationToken;
1249
1250 struct CaptureRequest(mpsc::Sender<RequestControlMessage>);
1252
1253 #[async_trait::async_trait]
1254 impl RequestPlaneClient for CaptureRequest {
1255 async fn send_request(
1256 &self,
1257 _address: String,
1258 payload: Bytes,
1259 _headers: super::super::unified_client::Headers,
1260 ) -> anyhow::Result<Bytes> {
1261 let message = TwoPartCodec::default().decode_message(payload)?;
1262 let control = serde_json::from_slice(&message.header)?;
1263 self.0.send(control).await?;
1264 Ok(Bytes::new())
1265 }
1266
1267 fn transport_name(&self) -> &'static str {
1268 "test"
1269 }
1270
1271 fn is_healthy(&self) -> bool {
1272 true
1273 }
1274 }
1275
1276 let shutdown = CancellationToken::new();
1277 let _shutdown_guard = shutdown.clone().drop_guard();
1278 tokio::time::timeout(Duration::from_secs(10), async {
1279 let address = "127.0.0.1:0".parse().unwrap();
1280 let responses =
1281 quic_response::QuicResponseServer::new(address, address, shutdown).unwrap();
1282 let callbacks = temp_env::async_with_vars(
1285 [
1286 ("DYN_TCP_TLS_CERT_PATH", None::<&str>),
1287 ("DYN_TCP_TLS_KEY_PATH", None),
1288 ("DYN_TCP_TLS_CLIENT_CA_CERT_PATH", None),
1289 ],
1290 tcp::server::TcpStreamServer::new(
1291 tcp::server::ServerOptions::builder()
1292 .interface(Some("127.0.0.1".to_string()))
1293 .build()
1294 .unwrap(),
1295 ),
1296 )
1297 .await
1298 .unwrap();
1299 let (request_tx, mut request_rx) = mpsc::channel(1);
1300 let router = AddressedPushRouter::new_quic(
1301 Arc::new(CaptureRequest(request_tx)),
1302 callbacks,
1303 responses,
1304 );
1305 let request = SingleIn::new(AddressedRequest::new(42_u64, "test".to_string()));
1306 let pool = quic_response::QuicResponseClientPool::from_env().unwrap();
1307 let (response, (mut sender, payload_codec)) =
1308 tokio::join!(router.generate(request), async {
1309 let control = request_rx.recv().await.unwrap();
1310 let worker_context =
1311 Context::with_id_and_metadata((), control.id, control.metadata);
1312 let mut sender = pool
1313 .sender(worker_context.context(), control.connection_info)
1314 .await
1315 .unwrap();
1316 sender.send_prologue(None).await.unwrap();
1317 (sender, control.payload_codec)
1318 });
1319
1320 let mut response: ManyOut<Annotated<u64>> = response.unwrap();
1323 for value in [1_u64, 2] {
1325 sender
1326 .send(
1327 payload_codec
1328 .encode(&super::NetworkStreamWrapper {
1329 data: Some(Annotated::from_data(value)),
1330 complete_final: value == 2,
1331 })
1332 .unwrap()
1333 .into(),
1334 )
1335 .await
1336 .unwrap();
1337 let item = response
1338 .next()
1339 .await
1340 .expect("QUIC stream ended before delivering data");
1341 assert!(
1342 item.error.is_none(),
1343 "unexpected QUIC response error: {:?}",
1344 item.error
1345 );
1346 assert_eq!(item.data, Some(value));
1347 }
1348 sender.finish().await.unwrap();
1349 assert!(
1350 response.next().await.is_none(),
1351 "QUIC stream must end after all data"
1352 );
1353 })
1354 .await
1355 .expect("QUIC router did not deliver the established response stream");
1356 }
1357
1358 #[test]
1359 fn legacy_worker_without_codec_metadata_receives_json() {
1360 let worker = Instance {
1361 component: "worker".to_string(),
1362 endpoint: "generate".to_string(),
1363 namespace: "default".to_string(),
1364 instance_id: 42,
1365 transport: TransportType::Nats("worker.generate".to_string()),
1366 device_type: None,
1367 request_plane_codec: None,
1368 };
1369 let payload_codec = payload_codec_for_worker(Some(&worker));
1370 assert_eq!(payload_codec, RequestPlanePayloadCodec::Json);
1371
1372 let request = TestRequest { value: 123 };
1373 let buffer = build_request_envelope(
1374 &Context::new(()),
1375 ConnectionInfo {
1376 transport: "tcp".to_string(),
1377 info: "{}".to_string(),
1378 },
1379 None,
1380 Some(&request),
1381 payload_codec,
1382 )
1383 .expect("legacy-worker request envelope should encode");
1384 let message = TwoPartCodec::default()
1385 .decode_message(buffer)
1386 .expect("request envelope should decode");
1387
1388 let control: RequestControlMessage = serde_json::from_slice(&message.header).unwrap();
1389 assert_eq!(control.payload_codec, RequestPlanePayloadCodec::Json);
1390 assert_eq!(
1391 serde_json::from_slice::<TestRequest>(&message.data).unwrap(),
1392 request
1393 );
1394 }
1395
1396 #[test]
1397 fn serialize_control_message_succeeds_under_limit() {
1398 let mut metadata = BTreeMap::new();
1399 metadata.insert("x-tiny-blob".to_string(), "alpha".to_string());
1400
1401 let ctrl = serialize_control_message(&base_control_message(metadata))
1402 .expect("control message should serialize under the limit");
1403 assert!(ctrl.len() <= CONTROL_MESSAGE_MAX_BYTES);
1404 }
1405
1406 #[test]
1407 fn serialize_control_message_errors_over_limit() {
1408 let mut metadata = BTreeMap::new();
1409 metadata.insert(
1410 "x-large-blob".to_string(),
1411 "x".repeat(CONTROL_MESSAGE_MAX_BYTES),
1412 );
1413
1414 let err = serialize_control_message(&base_control_message(metadata))
1415 .expect_err("oversized control message should fail")
1416 .to_string();
1417 assert!(err.contains("request control message too large"));
1418 assert!(err.contains(&CONTROL_MESSAGE_MAX_BYTES.to_string()));
1419 }
1420
1421 #[test]
1422 fn propagated_first_response_guard_is_taken_once() {
1423 let (guard_dropped_tx, mut guard_dropped_rx) = oneshot::channel();
1424 let mut source_context = Context::new(());
1425 attach_first_response_guard(
1426 &mut source_context,
1427 Arc::new(DropSignal(Some(guard_dropped_tx))),
1428 );
1429 let mut derived_context = Context::new(());
1430 propagate_first_response_guard(&source_context, &mut derived_context).unwrap();
1431
1432 let source_guard = source_context
1433 .get::<FirstResponseGuard>(FIRST_RESPONSE_GUARD_CONTEXT_KEY)
1434 .unwrap();
1435 let derived_guard = derived_context
1436 .get::<FirstResponseGuard>(FIRST_RESPONSE_GUARD_CONTEXT_KEY)
1437 .unwrap();
1438 let retained = derived_guard.take().expect("derived context should win");
1439 assert!(source_guard.take().is_none());
1440
1441 drop(retained);
1442 assert_eq!(guard_dropped_rx.try_recv(), Ok(()));
1443 }
1444
1445 #[tokio::test]
1446 async fn first_response_guard_defers_tcp_cancellation_until_prologue() {
1447 struct CapturingClient(tokio::sync::mpsc::Sender<ConnectionInfo>);
1448
1449 #[async_trait::async_trait]
1450 impl RequestPlaneClient for CapturingClient {
1451 async fn send_request(
1452 &self,
1453 _address: String,
1454 payload: bytes::Bytes,
1455 _headers: Headers,
1456 ) -> anyhow::Result<bytes::Bytes> {
1457 let message = TwoPartCodec::default().decode_message(payload)?;
1458 let control: RequestControlMessage = serde_json::from_slice(&message.header)?;
1459 self.0.send(control.connection_info).await?;
1460 Ok(bytes::Bytes::new())
1461 }
1462
1463 fn transport_name(&self) -> &'static str {
1464 "capturing"
1465 }
1466
1467 fn is_healthy(&self) -> bool {
1468 true
1469 }
1470 }
1471
1472 temp_env::async_with_vars(
1473 [
1474 ("DYN_TCP_TLS_CERT_PATH", None::<&str>),
1475 ("DYN_TCP_TLS_KEY_PATH", None),
1476 ("DYN_TCP_TLS_CLIENT_CA_CERT_PATH", None),
1477 ("DYN_TCP_TLS_CA_CERT_PATH", None),
1478 ("DYN_TCP_TLS_INSECURE", None),
1479 ("DYN_TCP_TLS_CLIENT_CERT_PATH", None),
1480 ("DYN_TCP_TLS_CLIENT_KEY_PATH", None),
1481 ],
1482 async {
1483 tokio::time::timeout(Duration::from_secs(5), async {
1484 let server = TcpStreamServer::new(
1485 TcpStreamServer::options_builder()
1486 .interface(Some("127.0.0.1".to_string()))
1487 .build()
1488 .unwrap(),
1489 )
1490 .await
1491 .unwrap();
1492 let (connection_tx, mut connection_rx) = tokio::sync::mpsc::channel(1);
1493 let router =
1494 AddressedPushRouter::new(Arc::new(CapturingClient(connection_tx)), server)
1495 .unwrap();
1496 let (guard_dropped_tx, mut guard_dropped_rx) = oneshot::channel();
1497 let mut request = Context::new(AddressedRequest::new(1_u64, "worker".into()));
1498 attach_first_response_guard(
1499 &mut request,
1500 Arc::new(DropSignal(Some(guard_dropped_tx))),
1501 );
1502 let context = request.context();
1503 let mut generation = tokio::spawn(async move {
1504 let response: ManyOut<Annotated<u64>> = router.generate(request).await?;
1505 Ok::<_, anyhow::Error>(response)
1506 });
1507 let connection_info = connection_rx.recv().await.unwrap();
1508 let worker_context = Context::with_id_and_metadata(
1509 (),
1510 context.id().to_string(),
1511 Default::default(),
1512 )
1513 .context();
1514 let mut sender = TcpClient::create_response_stream(
1515 worker_context.clone(),
1516 connection_info,
1517 None,
1518 )
1519 .await
1520 .unwrap();
1521
1522 for kill in [false, true] {
1523 if kill {
1524 context.kill();
1525 } else {
1526 context.stop_generating();
1527 }
1528 tokio::select! {
1529 _ = worker_context.stopped() => panic!("guarded worker was cancelled before prologue"),
1530 _ = &mut guard_dropped_rx => panic!("guard was released before prologue"),
1531 _ = &mut generation => panic!("guarded generation completed before prologue"),
1532 _ = tokio::time::sleep(Duration::from_millis(100)) => {}
1533 }
1534 assert!(!worker_context.is_stopped());
1535 assert_eq!(guard_dropped_rx.try_recv(), Err(TryRecvError::Empty));
1536 assert!(!generation.is_finished());
1537 }
1538
1539 sender
1540 .send_prologue(Some("worker setup failed".into()))
1541 .await
1542 .unwrap();
1543 let mut response = generation.await.unwrap().unwrap();
1544 assert!(response.context().is_killed());
1545 assert!(response.next().await.is_none());
1546 guard_dropped_rx.await.unwrap();
1547 })
1548 .await
1549 .expect("guarded TCP dispatch did not complete after the error prologue");
1550 },
1551 )
1552 .await;
1553 }
1554
1555 #[tokio::test]
1556 async fn first_response_guard_outlives_cancelled_dispatch_waiter() {
1557 let (raw_tx, raw_rx) = tokio::sync::mpsc::channel(1);
1558 let response_context = Context::new(()).context();
1559 let (dispatch_started_tx, dispatch_started_rx) = oneshot::channel();
1560 let (release_dispatch_tx, release_dispatch_rx) = oneshot::channel();
1561 let (guard_dropped_tx, mut guard_dropped_rx) = oneshot::channel();
1562 let mut source_context = Context::new(());
1563 attach_first_response_guard(
1564 &mut source_context,
1565 Arc::new(DropSignal(Some(guard_dropped_tx))),
1566 );
1567 let mut derived_context = Context::new(());
1568 propagate_first_response_guard(&source_context, &mut derived_context).unwrap();
1569 let guard = derived_context
1570 .get::<FirstResponseGuard>(FIRST_RESPONSE_GUARD_CONTEXT_KEY)
1571 .unwrap()
1572 .take()
1573 .unwrap();
1574 drop(source_context);
1575 drop(derived_context);
1576 let permits = Arc::new(Semaphore::new(1));
1577 let permit = try_acquire_retained_dispatch_permit(&permits).unwrap();
1578
1579 let waiter = tokio::spawn(dispatch_with_first_response_guard(
1580 async move {
1581 let _ = dispatch_started_tx.send(());
1582 let _ = release_dispatch_rx.await;
1583 let response: ManyOut<Annotated<u64>> =
1584 ResponseStream::new(Box::pin(ReceiverStream::new(raw_rx)), response_context);
1585 Ok(response)
1586 },
1587 guard,
1588 permit,
1589 ));
1590
1591 dispatch_started_rx.await.unwrap();
1592 waiter.abort();
1593 let _ = waiter.await;
1594 assert_eq!(guard_dropped_rx.try_recv(), Err(TryRecvError::Empty));
1595 let error = try_acquire_retained_dispatch_permit(&permits).unwrap_err();
1596 assert!(match_error_chain(
1597 error.as_ref(),
1598 &[ErrorType::ResourceExhausted],
1599 &[],
1600 ));
1601
1602 release_dispatch_tx.send(()).unwrap();
1603 raw_tx.send(Annotated::from_data(1_u64)).await.unwrap();
1604 tokio::time::timeout(Duration::from_secs(1), guard_dropped_rx)
1605 .await
1606 .expect("source guard was not released after the first worker response")
1607 .unwrap();
1608 let released_permit = tokio::time::timeout(Duration::from_secs(1), async {
1609 loop {
1610 if let Ok(permit) = try_acquire_retained_dispatch_permit(&permits) {
1611 break permit;
1612 }
1613 tokio::task::yield_now().await;
1614 }
1615 })
1616 .await
1617 .expect("retained dispatch permit was not released");
1618 drop(released_permit);
1619 }
1620
1621 #[tokio::test]
1622 async fn dropping_handed_off_tail_closes_upstream() {
1623 let (raw_tx, raw_rx) = tokio::sync::mpsc::channel(1);
1624 let response_context = Context::new(()).context();
1625 let (guard_dropped_tx, guard_dropped_rx) = oneshot::channel();
1626 let permits = Arc::new(Semaphore::new(1));
1627 let permit = try_acquire_retained_dispatch_permit(&permits).unwrap();
1628 let mut response = dispatch_with_first_response_guard(
1629 async move {
1630 let response: ManyOut<Annotated<u64>> =
1631 ResponseStream::new(Box::pin(ReceiverStream::new(raw_rx)), response_context);
1632 Ok(response)
1633 },
1634 Arc::new(DropSignal(Some(guard_dropped_tx))),
1635 permit,
1636 )
1637 .await
1638 .unwrap();
1639
1640 raw_tx.send(Annotated::from_data(1_u64)).await.unwrap();
1641 assert_eq!(response.next().await.unwrap().data, Some(1));
1642 tokio::time::timeout(Duration::from_secs(1), guard_dropped_rx)
1643 .await
1644 .expect("source guard was not released after the first worker response")
1645 .unwrap();
1646
1647 drop(response);
1648 tokio::time::timeout(Duration::from_secs(1), raw_tx.closed())
1649 .await
1650 .expect("dropping the caller stream did not close the upstream tail");
1651 }
1652}