1use super::{AsyncEngineContextProvider, ResponseStream};
5use crate::error::{BackendError, DynamoError, ErrorType, match_error_chain};
6use crate::{
7 component::{
8 Client, DeviceType, Endpoint, Instance, RoutingInstances, RoutingOccupancyState,
9 get_or_create_routing_occupancy_state,
10 },
11 discovery::EndpointInstanceId,
12 dynamo_nvtx_range,
13 engine::{AsyncEngine, AsyncEngineContext, Data},
14 metrics::frontend_perf::{STAGE_DURATION_SECONDS, STAGE_ROUTE},
15 pipeline::{
16 AddressedPushRouter, AddressedRequest, Error, ManyIn, ManyOut, SingleIn,
17 error::{PipelineError, PipelineErrorExt},
18 },
19 protocols::{EndpointId, maybe_error::MaybeError},
20 traits::DistributedRuntimeProvider,
21};
22use async_trait::async_trait;
23use futures::Stream;
24use rand::Rng;
25use serde::{Deserialize, Serialize};
26use std::{
27 collections::{HashMap, HashSet},
28 marker::PhantomData,
29 pin::Pin,
30 sync::{
31 Arc,
32 atomic::{AtomicU64, Ordering},
33 },
34 task::Poll,
35 time::Instant,
36};
37use tokio_stream::StreamExt;
38use tracing::Instrument;
39
40fn is_inhibited(err: &(dyn std::error::Error + 'static)) -> bool {
42 const INHIBITED: &[ErrorType] = &[
43 ErrorType::CannotConnect,
44 ErrorType::Disconnected,
45 ErrorType::ConnectionTimeout,
46 ErrorType::ResponseTimeout,
47 ErrorType::Backend(BackendError::EngineShutdown),
48 ];
49 match_error_chain(err, INHIBITED, &[])
50}
51
52fn response_inactivity_timeout() -> Option<std::time::Duration> {
56 use crate::config::environment_names::llm::DYN_HTTP_BACKEND_STREAM_TIMEOUT_SECS;
57 std::env::var(DYN_HTTP_BACKEND_STREAM_TIMEOUT_SECS)
58 .ok()
59 .and_then(|s| s.parse::<u64>().ok())
60 .filter(|&secs| secs > 0)
61 .map(std::time::Duration::from_secs)
62}
63
64struct OccupancyPermit {
68 state: Arc<RoutingOccupancyState>,
69 instance_id: u64,
70 armed: bool,
71}
72
73impl OccupancyPermit {
74 fn new(state: Arc<RoutingOccupancyState>, instance_id: u64) -> Self {
75 Self {
76 state,
77 instance_id,
78 armed: true,
79 }
80 }
81
82 fn retarget(&mut self, instance_id: u64) {
83 if self.instance_id == instance_id {
84 return;
85 }
86 self.state.increment(instance_id);
87 self.state.decrement(self.instance_id);
88 self.instance_id = instance_id;
89 }
90
91 fn into_tracked_stream<U: Data>(mut self, stream: ManyOut<U>) -> ManyOut<U> {
92 self.armed = false;
93 let engine_ctx = stream.context();
94 ResponseStream::new(
95 Box::pin(OccupancyTrackedStream {
96 inner: stream,
97 state: self.state.clone(),
98 instance_id: self.instance_id,
99 released: false,
100 }),
101 engine_ctx,
102 )
103 }
104}
105
106impl Drop for OccupancyPermit {
107 fn drop(&mut self) {
108 if self.armed {
109 self.state.decrement(self.instance_id);
110 }
111 }
112}
113
114#[async_trait]
117pub trait WorkerLoadMonitor: Send + Sync {
118 async fn start_monitoring(&self) -> anyhow::Result<()>;
121}
122
123pub trait MultimodalCacheIndex: Send + Sync {
125 fn workers_with_cache_key_hits(&self, cache_keys: &[String]) -> Vec<(u64, usize)>;
126 fn remove_worker(&self, worker_id: u64);
127}
128
129pub type MultimodalCacheKeyExtractor<T> = Arc<dyn Fn(&T) -> Vec<String> + Send + Sync>;
130
131#[derive(Clone)]
132pub struct PushRouter<T, U>
133where
134 T: Data + Serialize,
135 U: Data + for<'de> Deserialize<'de>,
136{
137 pub client: Client,
140
141 router_mode: RouterMode,
148
149 round_robin_counter: Arc<AtomicU64>,
151
152 addressed: Arc<AddressedPushRouter>,
155
156 fault_detection_enabled: bool,
161
162 response_timeout: Option<std::time::Duration>,
165
166 occupancy_state: Option<Arc<RoutingOccupancyState>>,
168
169 multimodal_cache_indexer: Option<Arc<dyn MultimodalCacheIndex>>,
172
173 multimodal_cache_key_extractor: Option<MultimodalCacheKeyExtractor<T>>,
175
176 _phantom: PhantomData<(T, U)>,
180}
181
182#[derive(Default, Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
183#[serde(rename_all = "snake_case")]
184pub enum RouterMode {
185 #[default]
186 RoundRobin,
187 Random,
188 PowerOfTwoChoices,
189 KV,
190 Direct,
191 LeastLoaded,
192 DeviceAwareWeighted,
194}
195
196#[derive(Clone, Copy)]
197enum TransportFallback<'a> {
198 Allow,
199 Deny,
200 Within(&'a HashSet<u64>),
201}
202
203struct DeviceAwareCandidates {
204 candidates: Vec<u64>,
205 device_type_map: HashMap<u64, Option<DeviceType>>,
206 embedding_cache_hit: bool,
207 full_embedding_cache_hit: bool,
208 request_cache_keys: usize,
209}
210
211impl RouterMode {
212 pub fn is_kv_routing(&self) -> bool {
213 *self == RouterMode::KV
214 }
215
216 pub fn is_direct_routing(&self) -> bool {
217 *self == RouterMode::Direct
218 }
219}
220
221fn p2c_select_from(occupancy_state: &RoutingOccupancyState, instance_ids: &[u64]) -> u64 {
224 let count = instance_ids.len();
225 if count == 1 {
226 let worker_id = instance_ids[0];
227 tracing::info!(
228 router_mode = "power-of-two-choices",
229 worker_id,
230 candidate_count = count,
231 load = occupancy_state.load(worker_id),
232 "Selected worker"
233 );
234 return worker_id;
235 }
236 let mut rng = rand::rng();
237 let idx1 = rng.random_range(0..count);
238 let idx2 = (idx1 + 1 + rng.random_range(0..count - 1)) % count;
239 let id1 = instance_ids[idx1];
240 let id2 = instance_ids[idx2];
241 let load1 = occupancy_state.load(id1);
242 let load2 = occupancy_state.load(id2);
243 let selected = if load1 <= load2 { id1 } else { id2 };
244 tracing::info!(
245 router_mode = "power-of-two-choices",
246 worker_id = selected,
247 candidate_count = count,
248 load = std::cmp::min(load1, load2),
249 candidate_a = id1,
250 candidate_a_load = load1,
251 candidate_b = id2,
252 candidate_b_load = load2,
253 "Selected worker"
254 );
255 selected
256}
257
258fn device_aware_candidate_group(
270 state: &RoutingOccupancyState,
271 instance_ids: &[u64],
272 device_type_map: &HashMap<u64, Option<DeviceType>>,
273 non_cpu_to_cpu_ratio: usize,
274) -> Vec<u64> {
275 let cpu_ids: Vec<u64> = instance_ids
276 .iter()
277 .copied()
278 .filter(|id| matches!(device_type_map.get(id), Some(Some(DeviceType::Cpu))))
279 .collect();
280 let non_cpu_ids: Vec<u64> = instance_ids
281 .iter()
282 .copied()
283 .filter(|id| !matches!(device_type_map.get(id), Some(Some(DeviceType::Cpu))))
284 .collect();
285
286 if cpu_ids.is_empty() {
287 return non_cpu_ids;
288 }
289 if non_cpu_ids.is_empty() {
290 return cpu_ids;
291 }
292
293 let total_non_cpu_inflight: u64 = non_cpu_ids.iter().map(|id| state.load(*id)).sum();
295 let total_cpu_inflight: u64 = cpu_ids.iter().map(|id| state.load(*id)).sum();
296 let cpu_count = cpu_ids.len() as u64;
297 let non_cpu_count = non_cpu_ids.len() as u64;
298 let allowed_cpu_inflight = total_non_cpu_inflight.saturating_mul(cpu_count)
299 / ((non_cpu_to_cpu_ratio as u64).saturating_mul(non_cpu_count));
300
301 if total_cpu_inflight < allowed_cpu_inflight {
302 cpu_ids
303 } else {
304 non_cpu_ids
305 }
306}
307
308static ENDPOINT_WATCHER_ACTIVE: std::sync::OnceLock<dashmap::DashMap<EndpointId, ()>> =
311 std::sync::OnceLock::new();
312
313static ENDPOINT_CACHE_INDEXER_WATCHER_ACTIVE: std::sync::OnceLock<
315 dashmap::DashMap<EndpointId, ()>,
316> = std::sync::OnceLock::new();
317
318fn spawn_instance_removal_watcher(
324 endpoint: Endpoint,
325 addressed: Arc<AddressedPushRouter>,
326 cancel_token: tokio_util::sync::CancellationToken,
327) {
328 use crate::discovery::{
329 DiscoveryEvent, DiscoveryInstance, DiscoveryInstanceId, DiscoveryQuery,
330 };
331 use tokio_stream::StreamExt as _;
332
333 let guard = ENDPOINT_WATCHER_ACTIVE.get_or_init(dashmap::DashMap::new);
335 let endpoint_id = endpoint.id();
336 if guard.insert(endpoint_id.clone(), ()).is_some() {
337 tracing::debug!(
338 ?endpoint_id,
339 "Instance removal watcher already running for this endpoint, skipping"
340 );
341 return;
342 }
343
344 let endpoint_name = endpoint.name().to_string();
345
346 tokio::spawn(async move {
347 struct GuardRelease(EndpointId);
350 impl Drop for GuardRelease {
351 fn drop(&mut self) {
352 if let Some(map) = ENDPOINT_WATCHER_ACTIVE.get() {
353 map.remove(&self.0);
354 }
355 }
356 }
357 let _release = GuardRelease(endpoint_id);
358
359 let namespace = endpoint.component().namespace().name();
360 let component = endpoint.component().name().to_string();
361
362 const RECONNECT_BACKOFF: std::time::Duration = std::time::Duration::from_secs(5);
364 'reconnect: loop {
365 let query = DiscoveryQuery::Endpoint {
366 namespace: namespace.clone(),
367 component: component.clone(),
368 endpoint: endpoint_name.clone(),
369 };
370
371 let mut stream = match endpoint.drt().discovery().list_and_watch(query, None).await {
372 Ok(s) => s,
373 Err(e) => {
374 tracing::warn!(
375 endpoint = %endpoint_name,
376 "Failed to start instance removal watcher (will retry): {e}"
377 );
378 tokio::select! {
379 _ = tokio::time::sleep(RECONNECT_BACKOFF) => continue 'reconnect,
380 _ = cancel_token.cancelled() => break 'reconnect,
381 }
382 }
383 };
384
385 loop {
386 tokio::select! {
387 event = stream.next() => {
388 match event {
389 Some(Ok(DiscoveryEvent::Removed(id))) => {
390 if let DiscoveryInstanceId::Endpoint(eid) = &id {
391 let n = addressed.cancel_instance_streams(eid).await;
392 if n > 0 {
393 tracing::warn!(
394 namespace = %eid.namespace,
395 component = %eid.component,
396 endpoint = %eid.endpoint,
397 instance_id = eid.instance_id,
398 cancelled = n,
399 "Cancelled pending response streams for removed \
400 instance (discovery-driven cleanup)"
401 );
402 }
403 }
404 }
405 Some(Ok(DiscoveryEvent::Added(DiscoveryInstance::Endpoint(inst)))) => {
406 let eid: EndpointInstanceId = inst.endpoint_instance_id();
407 addressed.clear_instance_tombstone(&eid).await;
408 }
409 Some(Ok(_)) => {}
410 Some(Err(e)) => {
411 tracing::warn!(
412 endpoint = %endpoint_name,
413 "Instance removal watcher stream error: {e}"
414 );
415 }
416 None => {
417 tracing::warn!(
418 endpoint = %endpoint_name,
419 "Instance removal watcher stream ended; reconnecting"
420 );
421 continue 'reconnect;
422 }
423 }
424 }
425 _ = cancel_token.cancelled() => {
426 break 'reconnect;
427 }
428 }
429 }
430 }
431
432 tracing::debug!(endpoint = %endpoint_name, "Instance removal watcher exiting");
433 });
434}
435
436fn spawn_multimodal_cache_cleanup_watcher(
438 endpoint: Endpoint,
439 indexer: Arc<dyn MultimodalCacheIndex>,
440 cancel_token: tokio_util::sync::CancellationToken,
441) {
442 use crate::discovery::{DiscoveryEvent, DiscoveryInstanceId, DiscoveryQuery};
443 use tokio_stream::StreamExt as _;
444
445 let guard = ENDPOINT_CACHE_INDEXER_WATCHER_ACTIVE.get_or_init(dashmap::DashMap::new);
446 let endpoint_id = endpoint.id();
447 if guard.insert(endpoint_id.clone(), ()).is_some() {
448 tracing::debug!(
449 ?endpoint_id,
450 "Multimodal cache cleanup watcher already running for this endpoint, skipping"
451 );
452 return;
453 }
454
455 let endpoint_name = endpoint.name().to_string();
456 let namespace = endpoint.component().namespace().name();
457 let component = endpoint.component().name().to_string();
458
459 tokio::spawn(async move {
460 struct GuardRelease(EndpointId);
461 impl Drop for GuardRelease {
462 fn drop(&mut self) {
463 if let Some(map) = ENDPOINT_CACHE_INDEXER_WATCHER_ACTIVE.get() {
464 map.remove(&self.0);
465 }
466 }
467 }
468 let _release = GuardRelease(endpoint_id);
469
470 const RECONNECT_BACKOFF: std::time::Duration = std::time::Duration::from_secs(5);
471 'reconnect: loop {
472 let query = DiscoveryQuery::Endpoint {
473 namespace: namespace.clone(),
474 component: component.clone(),
475 endpoint: endpoint_name.clone(),
476 };
477
478 let mut stream = match endpoint.drt().discovery().list_and_watch(query, None).await {
479 Ok(stream) => stream,
480 Err(error) => {
481 tracing::warn!(
482 endpoint = %endpoint_name,
483 "Failed to start multimodal cache cleanup watcher (will retry): {error}"
484 );
485 tokio::select! {
486 _ = tokio::time::sleep(RECONNECT_BACKOFF) => continue 'reconnect,
487 _ = cancel_token.cancelled() => break 'reconnect,
488 }
489 }
490 };
491
492 loop {
493 tokio::select! {
494 event = stream.next() => {
495 match event {
496 Some(Ok(DiscoveryEvent::Removed(DiscoveryInstanceId::Endpoint(eid)))) => {
497 indexer.remove_worker(eid.instance_id);
498 }
499 Some(Ok(_)) => {}
500 Some(Err(error)) => {
501 tracing::warn!(
502 endpoint = %endpoint_name,
503 "Multimodal cache cleanup watcher stream error: {error}"
504 );
505 continue 'reconnect;
506 }
507 None => {
508 tracing::warn!(
509 endpoint = %endpoint_name,
510 "Multimodal cache cleanup watcher stream ended; reconnecting"
511 );
512 continue 'reconnect;
513 }
514 }
515 }
516 _ = cancel_token.cancelled() => break 'reconnect,
517 }
518 }
519 }
520
521 tracing::debug!(endpoint = %endpoint_name, "Multimodal cache cleanup watcher exiting");
522 });
523}
524
525async fn addressed_router(endpoint: &Endpoint) -> anyhow::Result<Arc<AddressedPushRouter>> {
526 AddressedPushRouter::from_runtime_provider(endpoint).await
527}
528
529impl<T, U> PushRouter<T, U>
530where
531 T: Data + Serialize,
532 U: Data + for<'de> Deserialize<'de> + MaybeError,
533{
534 pub async fn from_client(client: Client, router_mode: RouterMode) -> anyhow::Result<Self> {
536 Self::from_client_with_monitor(client, router_mode, None).await
537 }
538
539 pub async fn from_client_no_fault_detection(
545 client: Client,
546 router_mode: RouterMode,
547 ) -> anyhow::Result<Self> {
548 let addressed = addressed_router(&client.endpoint).await?;
549
550 let occupancy_state = if matches!(
551 router_mode,
552 RouterMode::PowerOfTwoChoices
553 | RouterMode::LeastLoaded
554 | RouterMode::DeviceAwareWeighted
555 ) {
556 Some(get_or_create_routing_occupancy_state(&client.endpoint).await)
557 } else {
558 None
559 };
560
561 spawn_instance_removal_watcher(
563 client.endpoint.clone(),
564 addressed.clone(),
565 client.endpoint.drt().primary_token(),
566 );
567
568 Ok(PushRouter {
569 client,
570 addressed,
571 router_mode,
572 round_robin_counter: Arc::new(AtomicU64::new(0)),
573 fault_detection_enabled: false,
574 response_timeout: response_inactivity_timeout(),
575 occupancy_state,
576 multimodal_cache_indexer: None,
577 multimodal_cache_key_extractor: None,
578 _phantom: PhantomData,
579 })
580 }
581
582 pub async fn from_client_with_monitor(
589 client: Client,
590 router_mode: RouterMode,
591 worker_monitor: Option<Arc<dyn WorkerLoadMonitor>>,
592 ) -> anyhow::Result<Self> {
593 Self::from_client_with_state(client, router_mode, worker_monitor, None, None).await
594 }
595
596 pub async fn from_client_with_state(
598 client: Client,
599 router_mode: RouterMode,
600 worker_monitor: Option<Arc<dyn WorkerLoadMonitor>>,
601 multimodal_cache_indexer: Option<Arc<dyn MultimodalCacheIndex>>,
602 multimodal_cache_key_extractor: Option<MultimodalCacheKeyExtractor<T>>,
603 ) -> anyhow::Result<Self> {
604 let addressed = addressed_router(&client.endpoint).await?;
605
606 if let Some(monitor) = worker_monitor.as_ref() {
608 monitor.start_monitoring().await?;
609 }
610
611 let occupancy_state = if matches!(
612 router_mode,
613 RouterMode::PowerOfTwoChoices
614 | RouterMode::LeastLoaded
615 | RouterMode::DeviceAwareWeighted
616 ) {
617 Some(get_or_create_routing_occupancy_state(&client.endpoint).await)
618 } else {
619 None
620 };
621
622 spawn_instance_removal_watcher(
624 client.endpoint.clone(),
625 addressed.clone(),
626 client.endpoint.drt().primary_token(),
627 );
628
629 if let Some(indexer) = multimodal_cache_indexer.clone() {
631 spawn_multimodal_cache_cleanup_watcher(
632 client.endpoint.clone(),
633 indexer,
634 client.endpoint.drt().primary_token(),
635 );
636 }
637
638 let router = PushRouter {
639 client,
640 addressed,
641 router_mode,
642 round_robin_counter: Arc::new(AtomicU64::new(0)),
643 fault_detection_enabled: true,
644 response_timeout: response_inactivity_timeout(),
645 occupancy_state,
646 multimodal_cache_indexer,
647 multimodal_cache_key_extractor,
648 _phantom: PhantomData,
649 };
650
651 Ok(router)
652 }
653
654 fn empty_free_pool_error(&self, routing_instances: &RoutingInstances) -> anyhow::Error {
657 if !routing_instances.routable_ids().is_empty() {
658 let cause = PipelineError::ServiceOverloaded(
659 "All workers are busy, please retry later".to_string(),
660 );
661 return DynamoError::builder()
662 .error_type(ErrorType::ResourceExhausted)
663 .message("All workers are busy, please retry later")
664 .cause(cause)
665 .build()
666 .into();
667 }
668 DynamoError::builder()
669 .error_type(ErrorType::Unavailable)
670 .message(format!(
671 "No workers available for endpoint {}",
672 self.client.endpoint.id()
673 ))
674 .build()
675 .into()
676 }
677
678 pub async fn round_robin(&self, request: SingleIn<T>) -> anyhow::Result<ManyOut<U>> {
680 self.round_robin_prepared(request, |_, _| Ok(()))
681 .await
682 .map(|(_, stream)| stream)
683 }
684
685 async fn round_robin_prepared<M, F>(
686 &self,
687 request: SingleIn<T>,
688 prepare: F,
689 ) -> anyhow::Result<(M, ManyOut<U>)>
690 where
691 F: FnOnce(&mut T, u64) -> anyhow::Result<M>,
692 {
693 let counter = self.round_robin_counter.fetch_add(1, Ordering::Relaxed) as usize;
694
695 let (instance_id, candidate_count) = {
696 let routing_instances = self.client.routing_instances();
697 let count = routing_instances.free_ids().len();
698 if count == 0 {
699 return Err(self.empty_free_pool_error(&routing_instances));
700 }
701 (routing_instances.free_ids()[counter % count], count)
702 };
703 tracing::info!(
704 router_mode = "round-robin",
705 worker_id = instance_id,
706 candidate_count,
707 "Selected worker"
708 );
709
710 self.dispatch_selected(instance_id, request, None, prepare)
711 .await
712 }
713
714 pub async fn random(&self, request: SingleIn<T>) -> anyhow::Result<ManyOut<U>> {
716 self.random_prepared(request, |_, _| Ok(()))
717 .await
718 .map(|(_, stream)| stream)
719 }
720
721 async fn random_prepared<M, F>(
722 &self,
723 request: SingleIn<T>,
724 prepare: F,
725 ) -> anyhow::Result<(M, ManyOut<U>)>
726 where
727 F: FnOnce(&mut T, u64) -> anyhow::Result<M>,
728 {
729 let (instance_id, candidate_count) = {
730 let routing_instances = self.client.routing_instances();
731 let count = routing_instances.free_ids().len();
732 if count == 0 {
733 return Err(self.empty_free_pool_error(&routing_instances));
734 }
735 let counter = rand::rng().random::<u64>() as usize;
736 (routing_instances.free_ids()[counter % count], count)
737 };
738 tracing::info!(
739 router_mode = "random",
740 worker_id = instance_id,
741 candidate_count,
742 "Selected worker"
743 );
744
745 self.dispatch_selected(instance_id, request, None, prepare)
746 .await
747 }
748
749 pub async fn power_of_two_choices(&self, request: SingleIn<T>) -> anyhow::Result<ManyOut<U>> {
752 self.power_of_two_choices_prepared(request, |_, _| Ok(()))
753 .await
754 .map(|(_, stream)| stream)
755 }
756
757 async fn power_of_two_choices_prepared<M, F>(
758 &self,
759 request: SingleIn<T>,
760 prepare: F,
761 ) -> anyhow::Result<(M, ManyOut<U>)>
762 where
763 F: FnOnce(&mut T, u64) -> anyhow::Result<M>,
764 {
765 let state = self.occupancy_state()?;
766 let instance_id = {
767 let routing_instances = self.client.routing_instances();
768 if routing_instances.free_ids().is_empty() {
769 return Err(self.empty_free_pool_error(&routing_instances));
770 }
771 p2c_select_from(state.as_ref(), routing_instances.free_ids())
772 };
773 state.increment(instance_id);
774 let permit = OccupancyPermit::new(state, instance_id);
775 self.dispatch_selected(instance_id, request, Some(permit), prepare)
776 .await
777 }
778
779 pub async fn direct(
781 &self,
782 request: SingleIn<T>,
783 instance_id: u64,
784 ) -> anyhow::Result<ManyOut<U>> {
785 tracing::info!(
786 router_mode = "direct",
787 worker_id = instance_id,
788 "Selected worker"
789 );
790 self.generate_with_fault_detection(instance_id, request, TransportFallback::Deny)
791 .await
792 }
793
794 pub async fn direct_within(
801 &self,
802 request: SingleIn<T>,
803 instance_id: u64,
804 allowed_fallback: Option<&HashSet<u64>>,
805 ) -> anyhow::Result<ManyOut<U>> {
806 self.direct_within_prepared(request, instance_id, allowed_fallback, |_, _| Ok(()))
807 .await
808 .map(|(_, stream)| stream)
809 }
810
811 pub async fn direct_within_prepared<M, F>(
814 &self,
815 request: SingleIn<T>,
816 instance_id: u64,
817 allowed_fallback: Option<&HashSet<u64>>,
818 prepare: F,
819 ) -> anyhow::Result<(M, ManyOut<U>)>
820 where
821 F: FnOnce(&mut T, u64) -> anyhow::Result<M>,
822 {
823 if !self.client.instance_ids().contains(&instance_id) {
827 return Err(DynamoError::builder()
828 .error_type(ErrorType::CannotConnect)
829 .message(format!(
830 "instance_id={instance_id} not found for endpoint {}",
831 self.client.endpoint.id()
832 ))
833 .build()
834 .into());
835 }
836
837 tracing::info!(
838 router_mode = "direct",
839 worker_id = instance_id,
840 "Selected worker"
841 );
842
843 let fallback = allowed_fallback
844 .map(TransportFallback::Within)
845 .unwrap_or(TransportFallback::Allow);
846 self.generate_with_fault_detection_prepared(instance_id, request, fallback, prepare)
847 .await
848 }
849
850 pub async fn dispatch_exact(
855 &self,
856 request: SingleIn<T>,
857 instance_id: u64,
858 ) -> anyhow::Result<ManyOut<U>> {
859 self.generate_with_fault_detection(instance_id, request, TransportFallback::Deny)
860 .await
861 }
862
863 pub async fn select_and_dispatch_exact<M, F>(
866 &self,
867 mut request: SingleIn<T>,
868 pinned_worker: Option<u64>,
869 prepare: F,
870 ) -> anyhow::Result<(M, ManyOut<U>)>
871 where
872 F: FnOnce(&mut T, u64) -> anyhow::Result<M>,
873 {
874 let (instance_id, permit) = self
875 .select_exact_target(request.content(), pinned_worker)
876 .await?;
877 let metadata = prepare(&mut request, instance_id)?;
878 let stream = self.dispatch_exact(request, instance_id).await?;
879 let stream = match permit {
880 Some(permit) => permit.into_tracked_stream(stream),
881 None => stream,
882 };
883 Ok((metadata, stream))
884 }
885
886 pub async fn select_and_dispatch<M, F>(
889 &self,
890 request: SingleIn<T>,
891 prepare: F,
892 ) -> anyhow::Result<(M, ManyOut<U>)>
893 where
894 F: FnOnce(&mut T, u64) -> anyhow::Result<M>,
895 {
896 match self.router_mode {
897 RouterMode::Random => self.random_prepared(request, prepare).await,
898 RouterMode::RoundRobin => self.round_robin_prepared(request, prepare).await,
899 RouterMode::PowerOfTwoChoices => {
900 self.power_of_two_choices_prepared(request, prepare).await
901 }
902 RouterMode::LeastLoaded => self.least_loaded_prepared(request, prepare).await,
903 RouterMode::DeviceAwareWeighted => {
904 self.device_aware_weighted_prepared(request, prepare).await
905 }
906 RouterMode::KV => anyhow::bail!("KV routing should not call select_and_dispatch"),
907 RouterMode::Direct => anyhow::bail!(
908 "Direct routing should use direct_within_prepared instead of select_and_dispatch"
909 ),
910 }
911 }
912
913 async fn dispatch_selected<M, F>(
914 &self,
915 instance_id: u64,
916 request: SingleIn<T>,
917 mut permit: Option<OccupancyPermit>,
918 prepare: F,
919 ) -> anyhow::Result<(M, ManyOut<U>)>
920 where
921 F: FnOnce(&mut T, u64) -> anyhow::Result<M>,
922 {
923 let (metadata, stream) = self
924 .generate_with_fault_detection_prepared(
925 instance_id,
926 request,
927 TransportFallback::Allow,
928 |request, resolved_instance_id| {
929 if let Some(permit) = permit.as_mut() {
930 permit.retarget(resolved_instance_id);
931 }
932 prepare(request, resolved_instance_id)
933 },
934 )
935 .await?;
936 let stream = match permit {
937 Some(permit) => permit.into_tracked_stream(stream),
938 None => stream,
939 };
940 Ok((metadata, stream))
941 }
942
943 pub async fn device_aware_weighted(&self, request: SingleIn<T>) -> anyhow::Result<ManyOut<U>> {
952 self.device_aware_weighted_prepared(request, |_, _| Ok(()))
953 .await
954 .map(|(_, stream)| stream)
955 }
956
957 async fn device_aware_weighted_prepared<M, F>(
958 &self,
959 request: SingleIn<T>,
960 prepare: F,
961 ) -> anyhow::Result<(M, ManyOut<U>)>
962 where
963 F: FnOnce(&mut T, u64) -> anyhow::Result<M>,
964 {
965 let state = self.occupancy_state()?;
966 let routing_instances = self.client.routing_instances();
967 let instance_ids = routing_instances.free_ids().to_vec();
968
969 if instance_ids.is_empty() {
970 return Err(self.empty_free_pool_error(&routing_instances));
971 }
972
973 let endpoint_id = self.client.endpoint.id();
975
976 let selection =
977 self.device_aware_candidates(request.content(), state.as_ref(), &instance_ids);
978
979 let instance_id = if selection.full_embedding_cache_hit {
982 state.select_exact_min(&selection.candidates).await
983 } else {
984 state
985 .select_exact_min_and_increment(&selection.candidates)
986 .await
987 }
988 .ok_or_else(|| self.empty_free_pool_error(&routing_instances))?;
989 let permit = if selection.full_embedding_cache_hit {
990 None
991 } else {
992 Some(OccupancyPermit::new(state.clone(), instance_id))
993 };
994 let is_cpu = matches!(
995 selection.device_type_map.get(&instance_id),
996 Some(Some(DeviceType::Cpu))
997 );
998 tracing::info!(
999 router_mode = "device-aware-weighted",
1000 worker_id = instance_id,
1001 candidate_count = selection.candidates.len(),
1002 load = state.load(instance_id),
1003 endpoint = %endpoint_id,
1004 is_cpu,
1005 embedding_cache_hit = selection.embedding_cache_hit,
1006 request_cache_keys = selection.request_cache_keys,
1007 "Selected worker"
1008 );
1009
1010 self.dispatch_selected(instance_id, request, permit, prepare)
1011 .await
1012 }
1013
1014 fn device_aware_candidates(
1015 &self,
1016 request: &T,
1017 state: &RoutingOccupancyState,
1018 instance_ids: &[u64],
1019 ) -> DeviceAwareCandidates {
1020 let device_type_map = self
1021 .client
1022 .instances()
1023 .iter()
1024 .map(|instance| (instance.instance_id, instance.device_type.clone()))
1025 .collect();
1026 let cuda_to_cpu_ratio = std::env::var("DYN_ENCODER_CUDA_TO_CPU_RATIO")
1027 .ok()
1028 .and_then(|value| value.parse::<usize>().ok())
1029 .filter(|value| *value >= 1)
1030 .unwrap_or(8);
1031
1032 let (request_cache_keys, cache_matched_candidates) =
1033 if let (Some(indexer), Some(extractor)) = (
1034 self.multimodal_cache_indexer.as_ref(),
1035 self.multimodal_cache_key_extractor.as_ref(),
1036 ) {
1037 let request_cache_keys = extractor(request);
1038 let matched = if request_cache_keys.is_empty() {
1039 Vec::new()
1040 } else {
1041 let mut matched = indexer.workers_with_cache_key_hits(&request_cache_keys);
1042 matched.retain(|(id, _)| instance_ids.contains(id));
1043 matched
1044 };
1045 (request_cache_keys, matched)
1046 } else {
1047 (Vec::new(), Vec::new())
1048 };
1049
1050 let embedding_cache_hit = !cache_matched_candidates.is_empty();
1051 let request_cache_key_count = request_cache_keys
1052 .iter()
1053 .collect::<std::collections::HashSet<_>>()
1054 .len();
1055 let full_cache_candidates = cache_matched_candidates
1056 .iter()
1057 .filter_map(|(worker_id, hits)| {
1058 (*hits >= request_cache_key_count).then_some(*worker_id)
1059 })
1060 .collect::<Vec<_>>();
1061 let full_embedding_cache_hit = !full_cache_candidates.is_empty();
1062 let candidates = if full_embedding_cache_hit {
1063 full_cache_candidates
1064 } else {
1065 device_aware_candidate_group(state, instance_ids, &device_type_map, cuda_to_cpu_ratio)
1066 };
1067
1068 DeviceAwareCandidates {
1069 candidates,
1070 device_type_map,
1071 embedding_cache_hit,
1072 full_embedding_cache_hit,
1073 request_cache_keys: request_cache_keys.len(),
1074 }
1075 }
1076
1077 pub async fn least_loaded(&self, request: SingleIn<T>) -> anyhow::Result<ManyOut<U>> {
1079 self.least_loaded_prepared(request, |_, _| Ok(()))
1080 .await
1081 .map(|(_, stream)| stream)
1082 }
1083
1084 async fn least_loaded_prepared<M, F>(
1085 &self,
1086 request: SingleIn<T>,
1087 prepare: F,
1088 ) -> anyhow::Result<(M, ManyOut<U>)>
1089 where
1090 F: FnOnce(&mut T, u64) -> anyhow::Result<M>,
1091 {
1092 let state = self.occupancy_state()?;
1093 let routing_instances = self.client.routing_instances();
1094 let instance_ids = routing_instances.free_ids().to_vec();
1095 let instance_id = state
1096 .select_exact_min_and_increment(&instance_ids)
1097 .await
1098 .ok_or_else(|| self.empty_free_pool_error(&routing_instances))?;
1099 let permit = OccupancyPermit::new(state.clone(), instance_id);
1100 tracing::info!(
1101 router_mode = "least-loaded",
1102 worker_id = instance_id,
1103 candidate_count = instance_ids.len(),
1104 load = state.load(instance_id),
1105 "Selected worker"
1106 );
1107
1108 self.dispatch_selected(instance_id, request, Some(permit), prepare)
1109 .await
1110 }
1111
1112 pub fn select_next_worker(&self) -> Option<u64> {
1116 let routing_instances = self.client.routing_instances();
1117 let count = routing_instances.free_ids().len();
1118 if count == 0 {
1119 return None;
1120 }
1121
1122 match self.router_mode {
1123 RouterMode::RoundRobin => {
1124 let counter = self.round_robin_counter.fetch_add(1, Ordering::Relaxed) as usize;
1125 Some(routing_instances.free_ids()[counter % count])
1126 }
1127 RouterMode::Random => {
1128 let counter = rand::rng().random::<u64>() as usize;
1129 Some(routing_instances.free_ids()[counter % count])
1130 }
1131 RouterMode::PowerOfTwoChoices
1132 | RouterMode::Direct
1133 | RouterMode::LeastLoaded
1134 | RouterMode::DeviceAwareWeighted => None,
1135 RouterMode::KV => {
1136 panic!(
1137 "select_next_worker should not be called for {:?} routing mode",
1138 self.router_mode
1139 )
1140 }
1141 }
1142 }
1143
1144 pub fn peek_next_worker(&self) -> Option<u64> {
1150 let instance_ids = self.client.routing_instances().free_ids().to_vec();
1153 let count = instance_ids.len();
1154 if count == 0 {
1155 return None;
1156 }
1157
1158 match self.router_mode {
1159 RouterMode::RoundRobin => {
1160 let counter = self.round_robin_counter.load(Ordering::Relaxed) as usize;
1162 Some(instance_ids[counter % count])
1163 }
1164 RouterMode::Random => {
1165 let counter = rand::rng().random::<u64>() as usize;
1168 Some(instance_ids[counter % count])
1169 }
1170 RouterMode::LeastLoaded => self.occupancy_state.as_deref()?.peek_min(&instance_ids),
1171 RouterMode::PowerOfTwoChoices => Some(p2c_select_from(
1172 self.occupancy_state.as_deref()?,
1173 &instance_ids,
1174 )),
1175 RouterMode::DeviceAwareWeighted => {
1176 let state = self.occupancy_state.as_deref()?;
1177 let device_type_map: HashMap<u64, Option<DeviceType>> = self
1178 .client
1179 .instances()
1180 .iter()
1181 .map(|instance| (instance.instance_id, instance.device_type.clone()))
1182 .collect();
1183 let cuda_to_cpu_ratio = std::env::var("DYN_ENCODER_CUDA_TO_CPU_RATIO")
1184 .ok()
1185 .and_then(|value| value.parse::<usize>().ok())
1186 .filter(|value| *value >= 1)
1187 .unwrap_or(8);
1188 let candidates = device_aware_candidate_group(
1189 state,
1190 &instance_ids,
1191 &device_type_map,
1192 cuda_to_cpu_ratio,
1193 );
1194 state.peek_min(&candidates)
1195 }
1196 RouterMode::Direct => None,
1197 RouterMode::KV => {
1198 panic!(
1199 "peek_next_worker should not be called for {:?} routing mode",
1200 self.router_mode
1201 )
1202 }
1203 }
1204 }
1205
1206 async fn select_exact_target(
1207 &self,
1208 request: &T,
1209 pinned_worker: Option<u64>,
1210 ) -> anyhow::Result<(u64, Option<OccupancyPermit>)> {
1211 if let Some(instance_id) = pinned_worker {
1212 let routing_instances = self.client.routing_instances();
1213 if !routing_instances.routable_ids().contains(&instance_id) {
1214 return Err(anyhow::anyhow!(
1215 "instance_id={instance_id} not found for endpoint {}",
1216 self.client.endpoint.id()
1217 ));
1218 }
1219 let permit = match self.router_mode {
1220 RouterMode::LeastLoaded
1221 | RouterMode::PowerOfTwoChoices
1222 | RouterMode::DeviceAwareWeighted => {
1223 let state = self.occupancy_state()?;
1224 state.increment(instance_id);
1225 Some(OccupancyPermit::new(state, instance_id))
1226 }
1227 RouterMode::RoundRobin
1228 | RouterMode::Random
1229 | RouterMode::Direct
1230 | RouterMode::KV => None,
1231 };
1232 return Ok((instance_id, permit));
1233 }
1234
1235 match self.router_mode {
1236 RouterMode::LeastLoaded
1237 | RouterMode::PowerOfTwoChoices
1238 | RouterMode::DeviceAwareWeighted => {
1239 let state = self.occupancy_state()?;
1240 let routing_instances = self.client.routing_instances();
1241 let instance_ids = routing_instances.free_ids().to_vec();
1242 if instance_ids.is_empty() {
1243 return Err(self.empty_free_pool_error(&routing_instances));
1244 }
1245
1246 let instance_id = match self.router_mode {
1247 RouterMode::LeastLoaded => state
1248 .select_exact_min_and_increment(&instance_ids)
1249 .await
1250 .ok_or_else(|| self.empty_free_pool_error(&routing_instances))?,
1251 RouterMode::PowerOfTwoChoices => {
1252 let instance_id = p2c_select_from(state.as_ref(), &instance_ids);
1253 state.increment(instance_id);
1254 instance_id
1255 }
1256 RouterMode::DeviceAwareWeighted => {
1257 let selection =
1258 self.device_aware_candidates(request, state.as_ref(), &instance_ids);
1259 let instance_id = if selection.full_embedding_cache_hit {
1260 state.select_exact_min(&selection.candidates).await
1261 } else {
1262 state
1263 .select_exact_min_and_increment(&selection.candidates)
1264 .await
1265 }
1266 .ok_or_else(|| self.empty_free_pool_error(&routing_instances))?;
1267 let permit = (!selection.full_embedding_cache_hit)
1268 .then(|| OccupancyPermit::new(state, instance_id));
1269 return Ok((instance_id, permit));
1270 }
1271 _ => unreachable!(),
1272 };
1273 Ok((instance_id, Some(OccupancyPermit::new(state, instance_id))))
1274 }
1275 RouterMode::RoundRobin | RouterMode::Random => self
1276 .select_next_worker()
1277 .map(|instance_id| (instance_id, None))
1278 .ok_or_else(|| {
1279 let routing_instances = self.client.routing_instances();
1280 self.empty_free_pool_error(&routing_instances)
1281 }),
1282 RouterMode::Direct => Err(anyhow::anyhow!(
1283 "Worker ID required for exact dispatch in Direct routing mode"
1284 )),
1285 RouterMode::KV => Err(anyhow::anyhow!(
1286 "select_and_dispatch_exact cannot select workers in KV routing mode"
1287 )),
1288 }
1289 }
1290
1291 fn occupancy_state(&self) -> anyhow::Result<Arc<RoutingOccupancyState>> {
1292 self.occupancy_state.clone().ok_or_else(|| {
1293 anyhow::anyhow!(
1294 "routing occupancy state not initialized for endpoint {}",
1295 self.client.endpoint.id()
1296 )
1297 })
1298 }
1299
1300 async fn generate_with_fault_detection(
1311 &self,
1312 instance_id: u64,
1313 request: SingleIn<T>,
1314 fallback: TransportFallback<'_>,
1315 ) -> anyhow::Result<ManyOut<U>> {
1316 self.generate_with_fault_detection_prepared(instance_id, request, fallback, |_, _| Ok(()))
1317 .await
1318 .map(|(_, stream)| stream)
1319 }
1320
1321 async fn generate_with_fault_detection_prepared<M, F>(
1322 &self,
1323 instance_id: u64,
1324 mut request: SingleIn<T>,
1325 fallback: TransportFallback<'_>,
1326 prepare: F,
1327 ) -> anyhow::Result<(M, ManyOut<U>)>
1328 where
1329 F: FnOnce(&mut T, u64) -> anyhow::Result<M>,
1330 {
1331 let route_start = Instant::now();
1332 let request_id = request.id().to_string();
1333 let route_span = if matches!(self.router_mode, RouterMode::KV) {
1334 tracing::Span::none()
1335 } else {
1336 tracing::info_span!(
1337 "router.route_request",
1338 request_id = %request_id,
1339 worker_id = instance_id,
1340 router_mode = ?self.router_mode,
1341 )
1342 };
1343
1344 let (instance_id, address, transport_kind, instance) =
1345 self.resolve_transport(instance_id, fallback)?;
1346 self.check_workers_available(instance_id, &request_id)?;
1347
1348 let metadata = prepare(&mut request, instance_id)?;
1349 let request = request.map(|req| AddressedRequest::with_instance(req, address, instance));
1350
1351 STAGE_DURATION_SECONDS
1352 .with_label_values(&[STAGE_ROUTE])
1353 .observe(route_start.elapsed().as_secs_f64());
1354
1355 let _nvtx_transport = dynamo_nvtx_range!(transport_kind);
1356 let stream = self
1357 .addressed
1358 .generate(request)
1359 .instrument(route_span)
1360 .await;
1361 let stream = self.wrap_with_fault_detection(stream, instance_id)?;
1362 Ok((metadata, stream))
1363 }
1364
1365 fn check_workers_available(&self, instance_id: u64, request_id: &str) -> anyhow::Result<()> {
1370 if !self.fault_detection_enabled {
1371 return Ok(());
1372 }
1373 let routing_instances = self.client.routing_instances();
1374 let selected_worker_overloaded = routing_instances.is_overloaded(instance_id);
1375 let counts = routing_instances.counts();
1376 if tracing::enabled!(tracing::Level::DEBUG) {
1377 tracing::debug!(
1378 request_id,
1379 instance_id,
1380 router_mode = ?self.router_mode,
1381 free_workers = counts.free,
1382 overloaded_workers = counts.overloaded,
1383 total_workers = counts.discovered,
1384 selected_worker_overloaded,
1385 "checked worker overload state"
1386 );
1387 }
1388 if !selected_worker_overloaded {
1389 return Ok(());
1390 }
1391 tracing::warn!(
1392 instance_id,
1393 overloaded_workers = counts.overloaded,
1394 total_workers = counts.discovered,
1395 "Rejecting request: selected worker is overloaded"
1396 );
1397 let cause = PipelineError::ServiceOverloaded(
1398 "Selected worker is overloaded, please retry later".into(),
1399 );
1400 Err(DynamoError::builder()
1401 .error_type(ErrorType::ResourceExhausted)
1402 .message("Selected worker is overloaded, please retry later")
1403 .cause(cause)
1404 .build()
1405 .into())
1406 }
1407
1408 fn resolve_transport(
1414 &self,
1415 instance_id: u64,
1416 fallback: TransportFallback<'_>,
1417 ) -> anyhow::Result<(u64, String, &'static str, Instance)> {
1418 use crate::component::TransportType;
1419
1420 let lookup = |id: u64| {
1421 self.client
1422 .instances()
1423 .iter()
1424 .find(|i| i.instance_id == id)
1425 .map(|instance| {
1426 let (addr, kind) = match &instance.transport {
1427 TransportType::Tcp(tcp_endpoint) => {
1428 (tcp_endpoint.clone(), "transport.tcp.request")
1429 }
1430 TransportType::Nats(subject) => (subject.clone(), "transport.nats.request"),
1431 };
1432 (addr, kind, instance.clone())
1433 })
1434 };
1435
1436 if let Some((addr, kind, inst)) = lookup(instance_id) {
1437 return Ok((instance_id, addr, kind, inst));
1438 }
1439 let allowed_fallback = match fallback {
1440 TransportFallback::Allow => None,
1441 TransportFallback::Deny => {
1442 return Err(DynamoError::builder()
1443 .error_type(ErrorType::CannotConnect)
1444 .message(format!(
1445 "instance_id={instance_id} not found for endpoint {}",
1446 self.client.endpoint.id()
1447 ))
1448 .build()
1449 .into());
1450 }
1451 TransportFallback::Within(allowed) => Some(allowed),
1452 };
1453
1454 let routing_instances = self.client.routing_instances();
1455 let fallback_id = routing_instances.free_ids().iter().copied().find(|&id| {
1456 id != instance_id && allowed_fallback.is_none_or(|allowed| allowed.contains(&id))
1457 });
1458 match fallback_id {
1459 Some(id) => {
1460 tracing::warn!(
1461 original_instance = instance_id,
1462 fallback_instance = id,
1463 "Instance disappeared during routing, reselecting"
1464 );
1465 let (addr, kind, inst) = lookup(id).ok_or_else(|| {
1466 DynamoError::builder()
1467 .error_type(ErrorType::CannotConnect)
1468 .message(format!(
1469 "Fallback instance {} also not found for endpoint {}",
1470 id,
1471 self.client.endpoint.id()
1472 ))
1473 .build()
1474 })?;
1475 Ok((id, addr, kind, inst))
1476 }
1477 None => Err(anyhow::anyhow!(
1481 "Instance {} not found and no other instances available for endpoint {}",
1482 instance_id,
1483 self.client.endpoint.id()
1484 )),
1485 }
1486 }
1487
1488 fn wrap_with_fault_detection(
1493 &self,
1494 stream: anyhow::Result<ManyOut<U>>,
1495 instance_id: u64,
1496 ) -> anyhow::Result<ManyOut<U>> {
1497 let stream = match stream {
1498 Ok(stream) => stream,
1499 Err(err) => {
1500 if self.fault_detection_enabled {
1501 if is_inhibited(err.as_ref()) {
1502 tracing::debug!(
1503 "Reporting instance {instance_id} down due to error: {err}"
1504 );
1505 self.client.report_instance_down(instance_id);
1506 } else if match_error_chain(err.as_ref(), &[ErrorType::ResourceExhausted], &[])
1507 {
1508 tracing::debug!(
1514 "Marking instance {instance_id} overloaded due to backpressure: {err}"
1515 );
1516 self.client.mark_overloaded_immediate(instance_id);
1517 }
1518 }
1519 return Err(err);
1520 }
1521 };
1522
1523 if !self.fault_detection_enabled {
1524 return Ok(stream);
1525 }
1526
1527 let engine_ctx = stream.context();
1528 let client = self.client.clone();
1529 let client_for_timeout = self.client.clone();
1530 let stream = stream.map(move |res| {
1531 if let Some(err) = res.err()
1532 && is_inhibited(&err)
1533 {
1534 tracing::debug!(
1535 "Reporting instance {instance_id} down due to migratable error: {err}"
1536 );
1537 client.report_instance_down(instance_id);
1538 }
1539 res
1540 });
1541
1542 let stream: Pin<Box<dyn Stream<Item = U> + Send>> =
1543 if let Some(timeout) = self.response_timeout {
1544 Box::pin(async_stream::stream! {
1545 let mut inner = Box::pin(stream);
1546 loop {
1547 tokio::select! {
1548 biased;
1549 item = inner.next() => {
1550 match item {
1551 Some(item) => yield item,
1552 None => break,
1553 }
1554 }
1555 _ = tokio::time::sleep(timeout) => {
1556 tracing::warn!(
1557 instance_id,
1558 timeout_secs = timeout.as_secs(),
1559 "backend response inactivity timeout — quarantining worker"
1560 );
1561 client_for_timeout.report_instance_down(instance_id);
1562 yield U::from_err(
1563 crate::error::DynamoError::builder()
1564 .error_type(crate::error::ErrorType::ResponseTimeout)
1565 .message("backend response inactivity timeout")
1566 .build()
1567 );
1568 break;
1569 }
1570 }
1571 }
1572 })
1573 } else {
1574 Box::pin(stream)
1575 };
1576
1577 Ok(ResponseStream::new(stream, engine_ctx))
1578 }
1579}
1580
1581#[async_trait]
1582impl<T, U> AsyncEngine<SingleIn<T>, ManyOut<U>, Error> for PushRouter<T, U>
1583where
1584 T: Data + Serialize,
1585 U: Data + for<'de> Deserialize<'de> + MaybeError,
1586{
1587 async fn generate(&self, request: SingleIn<T>) -> Result<ManyOut<U>, Error> {
1588 match self.router_mode {
1589 RouterMode::Random => self.random(request).await,
1590 RouterMode::RoundRobin => self.round_robin(request).await,
1591 RouterMode::PowerOfTwoChoices => self.power_of_two_choices(request).await,
1592 RouterMode::KV => {
1593 anyhow::bail!("KV routing should not call generate on PushRouter");
1594 }
1595 RouterMode::Direct => {
1596 anyhow::bail!(
1597 "Direct routing should not call generate on PushRouter directly; use DirectRoutingRouter wrapper"
1598 );
1599 }
1600 RouterMode::LeastLoaded => self.least_loaded(request).await,
1601 RouterMode::DeviceAwareWeighted => self.device_aware_weighted(request).await,
1602 }
1603 }
1604}
1605
1606impl<T, U> PushRouter<T, U>
1607where
1608 T: Data + Serialize,
1609 U: Data + for<'de> Deserialize<'de> + MaybeError,
1610{
1611 async fn bidirectional_dispatch(
1613 &self,
1614 instance_id: u64,
1615 input: ManyIn<T>,
1616 ) -> anyhow::Result<ManyOut<U>> {
1617 let route_start = Instant::now();
1618 let request_id = input.context().id().to_string();
1619 let route_span = tracing::info_span!(
1620 "router.route_request_bidirectional",
1621 request_id = %request_id,
1622 worker_id = instance_id,
1623 router_mode = ?self.router_mode,
1624 );
1625
1626 let (instance_id, address, transport_kind, instance) =
1627 self.resolve_transport(instance_id, TransportFallback::Allow)?;
1628 self.check_workers_available(instance_id, &request_id)?;
1629
1630 STAGE_DURATION_SECONDS
1631 .with_label_values(&[STAGE_ROUTE])
1632 .observe(route_start.elapsed().as_secs_f64());
1633
1634 let _nvtx_transport = dynamo_nvtx_range!(transport_kind);
1635 let stream: anyhow::Result<ManyOut<U>> = self
1636 .addressed
1637 .generate_bidirectional(instance, address, input)
1638 .instrument(route_span)
1639 .await;
1640 self.wrap_with_fault_detection(stream, instance_id)
1641 }
1642}
1643
1644#[async_trait]
1658impl<T, U> AsyncEngine<ManyIn<T>, ManyOut<U>, Error> for PushRouter<T, U>
1659where
1660 T: Data + Serialize,
1661 U: Data + for<'de> Deserialize<'de> + MaybeError,
1662{
1663 async fn generate(&self, input: ManyIn<T>) -> Result<ManyOut<U>, Error> {
1664 match self.router_mode {
1665 RouterMode::KV => {
1666 anyhow::bail!("KV routing should not call generate on PushRouter");
1667 }
1668 RouterMode::Direct => {
1669 anyhow::bail!(
1670 "Direct routing should not call generate on PushRouter directly; use DirectRoutingRouter wrapper"
1671 );
1672 }
1673 RouterMode::PowerOfTwoChoices
1680 | RouterMode::LeastLoaded
1681 | RouterMode::DeviceAwareWeighted => {
1682 anyhow::bail!(
1683 "{:?} routing is not yet supported for bidirectional dispatch",
1684 self.router_mode
1685 );
1686 }
1687 RouterMode::RoundRobin | RouterMode::Random => {}
1688 }
1689
1690 let instance_id = self
1691 .select_next_worker()
1692 .ok_or_else(|| anyhow::anyhow!("no instances available for bidirectional routing"))?;
1693
1694 self.bidirectional_dispatch(instance_id, input).await
1695 }
1696}
1697
1698struct OccupancyTrackedStream<U: Data> {
1699 inner: ManyOut<U>,
1700 state: Arc<RoutingOccupancyState>,
1701 instance_id: u64,
1702 released: bool,
1703}
1704
1705impl<U: Data> Drop for OccupancyTrackedStream<U> {
1706 fn drop(&mut self) {
1707 if !self.released {
1708 self.state.decrement(self.instance_id);
1709 }
1710 }
1711}
1712
1713impl<U: Data> std::fmt::Debug for OccupancyTrackedStream<U> {
1714 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1715 f.debug_struct("OccupancyTrackedStream")
1716 .field("instance_id", &self.instance_id)
1717 .finish()
1718 }
1719}
1720
1721impl<U: Data> Stream for OccupancyTrackedStream<U> {
1722 type Item = U;
1723
1724 fn poll_next(
1725 mut self: Pin<&mut Self>,
1726 cx: &mut std::task::Context<'_>,
1727 ) -> Poll<Option<Self::Item>> {
1728 let poll = self.inner.as_mut().poll_next(cx);
1729 if matches!(poll, Poll::Ready(None)) && !self.released {
1730 self.state.decrement(self.instance_id);
1731 self.released = true;
1732 }
1733 poll
1734 }
1735}
1736
1737impl<U: Data> AsyncEngineContextProvider for OccupancyTrackedStream<U> {
1738 fn context(&self) -> Arc<dyn AsyncEngineContext> {
1739 self.inner.context()
1740 }
1741}
1742
1743impl<U: Data> crate::engine::AsyncEngineStream<U> for OccupancyTrackedStream<U> {}
1744
1745#[cfg(test)]
1746mod tests {
1747 use super::*;
1748 use crate::{
1749 DistributedRuntime, Runtime,
1750 distributed::DistributedConfig,
1751 error::DynamoError,
1752 pipeline::{
1753 RequestStream, ResponseStream,
1754 context::{Context, Controller},
1755 },
1756 };
1757 use serde::{Deserialize, Serialize};
1758
1759 #[derive(Clone, Debug, Deserialize, Serialize)]
1760 struct TestResponse {
1761 error: Option<DynamoError>,
1762 }
1763
1764 impl MaybeError for TestResponse {
1765 fn from_err(err: impl std::error::Error + 'static) -> Self {
1766 Self {
1767 error: Some(DynamoError::from(
1768 Box::new(err) as Box<dyn std::error::Error + 'static>
1769 )),
1770 }
1771 }
1772
1773 fn err(&self) -> Option<DynamoError> {
1774 self.error.clone()
1775 }
1776 }
1777
1778 fn assert_cannot_connect(error: &anyhow::Error) {
1779 assert!(
1780 match_error_chain(error.as_ref(), &[ErrorType::CannotConnect], &[]),
1781 "expected CannotConnect error, got: {error}"
1782 );
1783 assert!(
1784 !match_error_chain(error.as_ref(), &[ErrorType::ResourceExhausted], &[]),
1785 "CannotConnect failure must not be masked as ResourceExhausted: {error}"
1786 );
1787 }
1788
1789 fn assert_not_cannot_connect(error: &anyhow::Error) {
1790 assert!(
1791 !match_error_chain(error.as_ref(), &[ErrorType::CannotConnect], &[]),
1792 "fallback-enabled failure must preserve its existing error semantics: {error}"
1793 );
1794 }
1795
1796 struct StaticMultimodalCacheIndex {
1797 worker_id: u64,
1798 }
1799
1800 impl MultimodalCacheIndex for StaticMultimodalCacheIndex {
1801 fn workers_with_cache_key_hits(&self, cache_keys: &[String]) -> Vec<(u64, usize)> {
1802 vec![(self.worker_id, cache_keys.len())]
1803 }
1804
1805 fn remove_worker(&self, _worker_id: u64) {}
1806 }
1807
1808 #[test]
1809 fn p2c_selects_lower_load_worker() {
1810 let state = RoutingOccupancyState::default();
1811 for _ in 0..10 {
1812 state.increment(1);
1813 }
1814 state.increment(2);
1815
1816 let result = p2c_select_from(&state, &[1, 2]);
1818 assert_eq!(result, 2);
1819 }
1820
1821 #[test]
1822 fn p2c_selects_single_worker() {
1823 let state = RoutingOccupancyState::default();
1824 assert_eq!(p2c_select_from(&state, &[42]), 42);
1825 }
1826
1827 #[test]
1828 fn p2c_treats_missing_counts_as_zero() {
1829 let state = RoutingOccupancyState::default();
1830 for _ in 0..5 {
1831 state.increment(1);
1832 }
1833 let result = p2c_select_from(&state, &[1, 2]);
1835 assert_eq!(result, 2);
1836 }
1837
1838 #[test]
1839 fn p2c_returns_valid_worker_on_tie() {
1840 let state = RoutingOccupancyState::default();
1841 for _ in 0..3 {
1842 state.increment(1);
1843 state.increment(2);
1844 }
1845
1846 for _ in 0..100 {
1847 let result = p2c_select_from(&state, &[1, 2]);
1848 assert!(result == 1 || result == 2);
1849 }
1850 }
1851
1852 #[test]
1853 fn occupancy_permit_decrements_before_stream_creation() {
1854 let state = Arc::new(RoutingOccupancyState::default());
1855 state.increment(42);
1856 let permit = OccupancyPermit::new(state.clone(), 42);
1857 assert_eq!(state.load(42), 1);
1858 drop(permit);
1859 assert_eq!(state.load(42), 0);
1860 }
1861
1862 #[test]
1863 fn occupancy_tracked_stream_decrements_on_drop() {
1864 let state = Arc::new(RoutingOccupancyState::default());
1865 state.increment(7);
1866 let permit = OccupancyPermit::new(state.clone(), 7);
1867 let ctx: Arc<dyn AsyncEngineContext> = Arc::new(Controller::default());
1868 let stream = permit.into_tracked_stream(ResponseStream::new(
1869 Box::pin(tokio_stream::iter(vec![1u64])),
1870 ctx,
1871 ));
1872 assert_eq!(state.load(7), 1);
1873 drop(stream);
1874 assert_eq!(state.load(7), 0);
1875 }
1876
1877 #[tokio::test]
1878 async fn occupancy_tracked_stream_decrements_on_completion() {
1879 let state = Arc::new(RoutingOccupancyState::default());
1880 state.increment(7);
1881 let permit = OccupancyPermit::new(state.clone(), 7);
1882 let ctx: Arc<dyn AsyncEngineContext> = Arc::new(Controller::default());
1883 let mut stream = permit.into_tracked_stream(ResponseStream::new(
1884 Box::pin(tokio_stream::iter(vec![1u64])),
1885 ctx,
1886 ));
1887
1888 assert_eq!(stream.next().await, Some(1));
1889 assert_eq!(state.load(7), 1);
1890 assert_eq!(stream.next().await, None);
1891 assert_eq!(state.load(7), 0);
1892 drop(stream);
1893 assert_eq!(state.load(7), 0, "drop must not release twice after EOF");
1894 }
1895
1896 #[test]
1897 fn p2c_lifecycle_tracks_inflight_counts_with_shared_tracker() {
1898 let state = Arc::new(RoutingOccupancyState::default());
1899 let mut permits = Vec::new();
1900 for _ in 0..5 {
1901 let selected = p2c_select_from(&state, &[1, 2]);
1902 state.increment(selected);
1903 permits.push(OccupancyPermit::new(state.clone(), selected));
1904 }
1905
1906 let total = state.load(1) + state.load(2);
1907 assert_eq!(total, 5, "5 in-flight requests should be tracked");
1908
1909 drop(permits);
1910 let total = state.load(1) + state.load(2);
1911 assert_eq!(total, 0, "All guards dropped, counts should be 0");
1912 }
1913
1914 #[test]
1915 fn p2c_never_selects_dominated_worker() {
1916 let state = RoutingOccupancyState::default();
1917 for _ in 0..100 {
1918 state.increment(3);
1919 }
1920
1921 let mut selected = [0u32; 3];
1922 for _ in 0..1000 {
1923 let result = p2c_select_from(&state, &[1, 2, 3]);
1924 match result {
1925 1 => selected[0] += 1,
1926 2 => selected[1] += 1,
1927 3 => selected[2] += 1,
1928 _ => panic!("unexpected worker id"),
1929 }
1930 }
1931 assert_eq!(
1932 selected[2], 0,
1933 "Worker 3 (load=100) should never be selected against load=0 workers, but got {} times",
1934 selected[2]
1935 );
1936 }
1937
1938 #[tokio::test]
1939 async fn least_loaded_selects_exact_min_and_tracks_counts() {
1940 let state = Arc::new(RoutingOccupancyState::default());
1941 state.increment(1);
1942 state.increment(1);
1943 state.increment(2);
1944
1945 let selected = state
1946 .select_exact_min_and_increment(&[1, 2, 3])
1947 .await
1948 .unwrap();
1949 assert_eq!(selected, 3);
1950
1951 let permit = OccupancyPermit::new(state.clone(), selected);
1952 assert_eq!(state.load(selected), 1);
1953 drop(permit);
1954 assert_eq!(state.load(selected), 0);
1955 }
1956
1957 #[tokio::test]
1958 async fn bidirectional_generate_bails_with_no_instances() {
1959 let rt = Runtime::from_current().unwrap();
1960 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1961 .await
1962 .unwrap();
1963 let ns = drt.namespace("test_bidi_no_instances".to_string()).unwrap();
1964 let component = ns.component("test_component".to_string()).unwrap();
1965 let endpoint = component.endpoint("test_endpoint".to_string());
1966 let client = endpoint.client().await.unwrap();
1967
1968 let router = PushRouter::<u64, TestResponse>::from_client(client, RouterMode::RoundRobin)
1969 .await
1970 .unwrap();
1971
1972 let input: ManyIn<u64> =
1973 Context::new(RequestStream::new(Box::pin(tokio_stream::iter(vec![
1974 1u64, 2u64,
1975 ]))));
1976 let result = router.generate(input).await;
1977 assert!(
1978 result.is_err(),
1979 "bidirectional generate must bail when no instances are registered"
1980 );
1981
1982 rt.shutdown();
1983 }
1984
1985 #[tokio::test]
1986 async fn bidirectional_generate_bails_for_kv_router_mode() {
1987 let rt = Runtime::from_current().unwrap();
1988 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1989 .await
1990 .unwrap();
1991 let ns = drt.namespace("test_bidi_kv_mode".to_string()).unwrap();
1992 let component = ns.component("test_component".to_string()).unwrap();
1993 let endpoint = component.endpoint("test_endpoint".to_string());
1994 let client = endpoint.client().await.unwrap();
1995
1996 let router = PushRouter::<u64, TestResponse>::from_client(client, RouterMode::KV)
1997 .await
1998 .unwrap();
1999
2000 let input: ManyIn<u64> =
2001 Context::new(RequestStream::new(Box::pin(tokio_stream::iter(vec![1u64]))));
2002 let result = router.generate(input).await;
2003 assert!(
2004 result.is_err(),
2005 "bidirectional generate must bail for RouterMode::KV"
2006 );
2007 let err_msg = format!("{:?}", result.unwrap_err());
2008 assert!(
2009 err_msg.contains("KV") || err_msg.contains("kv"),
2010 "error should mention KV: got {err_msg}"
2011 );
2012
2013 rt.shutdown();
2014 }
2015
2016 #[tokio::test]
2017 async fn bidirectional_generate_bails_for_direct_router_mode() {
2018 let rt = Runtime::from_current().unwrap();
2019 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2020 .await
2021 .unwrap();
2022 let ns = drt.namespace("test_bidi_direct_mode".to_string()).unwrap();
2023 let component = ns.component("test_component".to_string()).unwrap();
2024 let endpoint = component.endpoint("test_endpoint".to_string());
2025 let client = endpoint.client().await.unwrap();
2026
2027 let router = PushRouter::<u64, TestResponse>::from_client(client, RouterMode::Direct)
2028 .await
2029 .unwrap();
2030
2031 let input: ManyIn<u64> =
2032 Context::new(RequestStream::new(Box::pin(tokio_stream::iter(vec![1u64]))));
2033 let result = router.generate(input).await;
2034 assert!(
2035 result.is_err(),
2036 "bidirectional generate must bail for RouterMode::Direct"
2037 );
2038 let err_msg = format!("{:?}", result.unwrap_err());
2039 assert!(
2040 err_msg.contains("Direct") || err_msg.contains("direct"),
2041 "error should mention Direct: got {err_msg}"
2042 );
2043
2044 rt.shutdown();
2045 }
2046
2047 #[tokio::test]
2048 async fn bidirectional_generate_rejects_unsupported_load_aware_modes() {
2049 let rt = Runtime::from_current().unwrap();
2050 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2051 .await
2052 .unwrap();
2053 let ns = drt.namespace("test_bidi_load_aware".to_string()).unwrap();
2054 let component = ns.component("test_component".to_string()).unwrap();
2055
2056 for mode in [
2057 RouterMode::PowerOfTwoChoices,
2058 RouterMode::LeastLoaded,
2059 RouterMode::DeviceAwareWeighted,
2060 ] {
2061 let endpoint = component.endpoint("test_endpoint".to_string());
2062 let client = endpoint.client().await.unwrap();
2063 let router = PushRouter::<u64, TestResponse>::from_client(client, mode)
2064 .await
2065 .unwrap();
2066
2067 let input: ManyIn<u64> =
2068 Context::new(RequestStream::new(Box::pin(tokio_stream::iter(vec![1u64]))));
2069 let result = router.generate(input).await;
2070 assert!(
2071 result.is_err(),
2072 "bidirectional generate must reject {mode:?} (not yet supported)"
2073 );
2074 let err_msg = format!("{:?}", result.unwrap_err());
2075 assert!(
2076 err_msg.contains("not yet supported for bidirectional dispatch"),
2077 "error should explain the mode is unsupported, not 'no instances': got {err_msg}"
2078 );
2079 }
2080
2081 rt.shutdown();
2082 }
2083
2084 #[tokio::test]
2085 async fn least_loaded_peek_returns_available_worker_select_stays_none() {
2086 let rt = Runtime::from_current().unwrap();
2087 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2088 .await
2089 .unwrap();
2090 let ns = drt
2091 .namespace("test_least_loaded_router".to_string())
2092 .unwrap();
2093 let component = ns.component("test_component".to_string()).unwrap();
2094 let endpoint = component.endpoint("test_endpoint".to_string());
2095 let client = endpoint.client().await.unwrap();
2096
2097 endpoint.register_endpoint_instance().await.unwrap();
2098 client.wait_for_instances().await.unwrap();
2099
2100 let router = PushRouter::<u64, TestResponse>::from_client(client, RouterMode::LeastLoaded)
2101 .await
2102 .unwrap();
2103
2104 assert_eq!(router.select_next_worker(), None);
2107 assert!(
2108 router.peek_next_worker().is_some(),
2109 "LeastLoaded peek must return the available worker for disagg bootstrap"
2110 );
2111
2112 rt.shutdown();
2113 }
2114
2115 #[tokio::test]
2116 async fn exact_selection_releases_occupancy_when_preparation_fails() {
2117 let rt = Runtime::from_current().unwrap();
2118 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2119 .await
2120 .unwrap();
2121 let ns = drt
2122 .namespace("test_exact_prepare_failure".to_string())
2123 .unwrap();
2124 let component = ns.component("test_component".to_string()).unwrap();
2125 let endpoint = component.endpoint("test_endpoint".to_string());
2126 let client = endpoint.client().await.unwrap();
2127 endpoint.register_endpoint_instance().await.unwrap();
2128 let worker_id = client.wait_for_instances().await.unwrap()[0].id();
2129
2130 let router = PushRouter::<u64, TestResponse>::from_client(client, RouterMode::LeastLoaded)
2131 .await
2132 .unwrap();
2133 let state = router.occupancy_state.clone().unwrap();
2134 let result = router
2135 .select_and_dispatch_exact(SingleIn::new(42), None, |_, _| {
2136 Err::<(), _>(anyhow::anyhow!("metadata preparation failed"))
2137 })
2138 .await;
2139
2140 assert!(result.is_err());
2141 assert_eq!(
2142 state.load(worker_id),
2143 0,
2144 "preparation failure must release the selected worker"
2145 );
2146 rt.shutdown();
2147 }
2148
2149 #[tokio::test]
2150 async fn exact_dispatch_revalidates_overload_after_preparation() {
2151 let rt = Runtime::from_current().unwrap();
2152 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2153 .await
2154 .unwrap();
2155 let ns = drt
2156 .namespace("test_exact_overload_revalidation".to_string())
2157 .unwrap();
2158 let component = ns.component("test_component".to_string()).unwrap();
2159 let endpoint = component.endpoint("test_endpoint".to_string());
2160 let client = endpoint.client().await.unwrap();
2161 endpoint.register_endpoint_instance().await.unwrap();
2162 let worker_id = client.wait_for_instances().await.unwrap()[0].id();
2163
2164 let router =
2165 PushRouter::<u64, TestResponse>::from_client(client.clone(), RouterMode::LeastLoaded)
2166 .await
2167 .unwrap();
2168 let state = router.occupancy_state.clone().unwrap();
2169 let result = router
2170 .select_and_dispatch_exact(SingleIn::new(42), Some(worker_id), |_, worker_id| {
2171 client.set_overloaded_instances(&[worker_id]);
2172 Ok(())
2173 })
2174 .await;
2175
2176 assert!(result.is_err());
2177 assert_eq!(
2178 state.load(worker_id),
2179 0,
2180 "validation failure must release the selected worker"
2181 );
2182 rt.shutdown();
2183 }
2184
2185 #[tokio::test]
2186 async fn transport_resolution_precedes_stale_overload_check() {
2187 let rt = Runtime::from_current().unwrap();
2188 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2189 .await
2190 .unwrap();
2191 let endpoint = drt
2192 .namespace("test_transport_precedes_stale_overload".to_string())
2193 .unwrap()
2194 .component("test_component".to_string())
2195 .unwrap()
2196 .endpoint("test_endpoint".to_string());
2197 let client = endpoint.client().await.unwrap();
2198 let stale_id = 99999;
2199 client.override_instance_avail(vec![stale_id]);
2200 client.set_overloaded_instances(&[stale_id]);
2201 let router = PushRouter::<u64, TestResponse>::from_client(client, RouterMode::RoundRobin)
2202 .await
2203 .unwrap();
2204
2205 let unary_error = router
2206 .direct(SingleIn::new(42), stale_id)
2207 .await
2208 .unwrap_err();
2209 assert_cannot_connect(&unary_error);
2210
2211 let input: ManyIn<u64> =
2212 Context::new(RequestStream::new(Box::pin(tokio_stream::iter(vec![1u64]))));
2213 let bidirectional_error = router
2214 .bidirectional_dispatch(stale_id, input)
2215 .await
2216 .unwrap_err();
2217 assert_not_cannot_connect(&bidirectional_error);
2218 assert!(
2219 !match_error_chain(
2220 bidirectional_error.as_ref(),
2221 &[ErrorType::ResourceExhausted],
2222 &[]
2223 ),
2224 "transport resolution must precede the stale overload check: {bidirectional_error}"
2225 );
2226
2227 rt.shutdown();
2228 }
2229
2230 #[tokio::test]
2231 async fn selected_overloaded_worker_is_rejected_before_dispatch() {
2232 const TEST_RECONCILE_INTERVAL: std::time::Duration = std::time::Duration::from_millis(50);
2233
2234 let rt = Runtime::from_current().unwrap();
2235 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2236 .await
2237 .unwrap();
2238 let ns = drt
2239 .namespace("test_selected_overloaded_worker_rejected".to_string())
2240 .unwrap();
2241 let component = ns.component("test_component".to_string()).unwrap();
2242 let endpoint = component.endpoint("test_endpoint".to_string());
2243 let client = Client::with_reconcile_interval(endpoint.clone(), TEST_RECONCILE_INTERVAL)
2244 .await
2245 .unwrap();
2246
2247 endpoint.register_endpoint_instance().await.unwrap();
2248 let instances = client.wait_for_instances().await.unwrap();
2249 let worker_id = instances[0].id();
2250
2251 for _ in 0..10 {
2252 if client.instance_ids_avail().contains(&worker_id) {
2253 break;
2254 }
2255 tokio::time::sleep(TEST_RECONCILE_INTERVAL).await;
2256 }
2257 assert!(
2258 client.instance_ids_avail().contains(&worker_id),
2259 "worker should be routable before marking it overloaded"
2260 );
2261
2262 client.set_overloaded_instances(&[worker_id]);
2263 let router = PushRouter::<u64, TestResponse>::from_client(client, RouterMode::RoundRobin)
2264 .await
2265 .unwrap();
2266
2267 let result = router.generate(SingleIn::new(42u64)).await;
2268 assert!(result.is_err());
2269 let msg = format!("{}", result.unwrap_err());
2270 assert!(
2276 msg.contains("All workers are busy"),
2277 "expected empty-free-pool rejection, got: {msg}"
2278 );
2279
2280 rt.shutdown();
2281 }
2282
2283 #[tokio::test]
2284 async fn direct_within_rejects_overloaded_constrained_target() {
2285 const TEST_RECONCILE_INTERVAL: std::time::Duration = std::time::Duration::from_secs(3600);
2286
2287 let rt = Runtime::from_current().unwrap();
2288 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2289 .await
2290 .unwrap();
2291 let ns = drt
2292 .namespace("test_direct_within_overload_rejection".to_string())
2293 .unwrap();
2294 let component = ns.component("test_component".to_string()).unwrap();
2295 let endpoint = component.endpoint("test_endpoint".to_string());
2296 let client = Client::with_reconcile_interval(endpoint.clone(), TEST_RECONCILE_INTERVAL)
2297 .await
2298 .unwrap();
2299
2300 endpoint.register_endpoint_instance().await.unwrap();
2301 let worker_id = client.wait_for_instances().await.unwrap()[0].id();
2302 client.set_overloaded_instances(&[worker_id]);
2303
2304 let router = PushRouter::<u64, TestResponse>::from_client(client, RouterMode::RoundRobin)
2305 .await
2306 .unwrap();
2307 let allowed = HashSet::from([worker_id]);
2308 let error = router
2309 .direct_within(SingleIn::new(42), worker_id, Some(&allowed))
2310 .await
2311 .unwrap_err();
2312
2313 assert!(match_error_chain(
2314 error.as_ref(),
2315 &[ErrorType::ResourceExhausted],
2316 &[]
2317 ));
2318 assert!(
2319 error.to_string().contains("Selected worker is overloaded"),
2320 "expected overload rejection, got: {error}"
2321 );
2322
2323 rt.shutdown();
2324 }
2325
2326 #[tokio::test]
2327 async fn no_workers_is_reported_as_unavailable() {
2328 let rt = Runtime::from_current().unwrap();
2329 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2330 .await
2331 .unwrap();
2332 let ns = drt
2333 .namespace("test_no_workers_unavailable".to_string())
2334 .unwrap();
2335 let component = ns.component("test_component".to_string()).unwrap();
2336 let endpoint = component.endpoint("test_endpoint".to_string());
2337 let client = endpoint.client().await.unwrap();
2338 let router = PushRouter::<u64, TestResponse>::from_client(client, RouterMode::RoundRobin)
2339 .await
2340 .unwrap();
2341
2342 let error = router.generate(SingleIn::new(42)).await.unwrap_err();
2343 assert!(match_error_chain(
2344 error.as_ref(),
2345 &[ErrorType::Unavailable],
2346 &[]
2347 ));
2348
2349 rt.shutdown();
2350 }
2351
2352 #[tokio::test]
2353 async fn round_robin_excludes_overloaded_workers_from_candidates() {
2354 const TEST_RECONCILE_INTERVAL: std::time::Duration = std::time::Duration::from_secs(3600);
2360
2361 let rt = Runtime::from_current().unwrap();
2362 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2363 .await
2364 .unwrap();
2365 let ns = drt
2366 .namespace("test_round_robin_excludes_overloaded".to_string())
2367 .unwrap();
2368 let component = ns.component("test_component".to_string()).unwrap();
2369 let endpoint = component.endpoint("test_endpoint".to_string());
2370 let client = Client::with_reconcile_interval(endpoint.clone(), TEST_RECONCILE_INTERVAL)
2371 .await
2372 .unwrap();
2373
2374 endpoint.register_endpoint_instance().await.unwrap();
2375 let instances = client.wait_for_instances().await.unwrap();
2376 let real_id = instances[0].id();
2377 for _ in 0..50 {
2378 if client.instance_ids_avail().contains(&real_id) {
2379 break;
2380 }
2381 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
2382 }
2383
2384 client.override_instance_avail(vec![1, 2]);
2391 client.set_overloaded_instances(&[1]);
2392
2393 let router = PushRouter::<u64, TestResponse>::from_client(client, RouterMode::RoundRobin)
2394 .await
2395 .unwrap();
2396
2397 for _ in 0..6 {
2400 let selected = router
2401 .peek_next_worker()
2402 .expect("peek should succeed with a free worker");
2403 assert_eq!(
2404 selected, 2,
2405 "overloaded worker 1 must not appear in the candidate set"
2406 );
2407 }
2408
2409 rt.shutdown();
2410 }
2411
2412 #[tokio::test]
2413 async fn device_aware_cpu_only_selects_least_loaded_instance() {
2414 let state = RoutingOccupancyState::default();
2415 for _ in 0..3 {
2417 state.increment(1);
2418 }
2419 state.increment(3);
2420
2421 let instance_ids = vec![1, 2, 3];
2422 let device_type_map = HashMap::from([
2423 (1, Some(DeviceType::Cpu)),
2424 (2, Some(DeviceType::Cpu)),
2425 (3, Some(DeviceType::Cpu)),
2426 ]);
2427
2428 let candidates = device_aware_candidate_group(&state, &instance_ids, &device_type_map, 8);
2429 assert_eq!(candidates, vec![1, 2, 3]);
2430
2431 let selected = state
2432 .select_exact_min_and_increment(&candidates)
2433 .await
2434 .unwrap();
2435 assert_eq!(selected, 2);
2436 }
2437
2438 #[tokio::test]
2439 async fn device_aware_non_cpu_only_selects_least_loaded_instance() {
2440 let state = RoutingOccupancyState::default();
2441 for _ in 0..3 {
2443 state.increment(1);
2444 }
2445 state.increment(3);
2446
2447 let instance_ids = vec![1, 2, 3];
2448 let device_type_map = HashMap::from([
2449 (1, Some(DeviceType::Cuda)),
2450 (2, Some(DeviceType::Cuda)),
2451 (3, Some(DeviceType::Cuda)),
2452 ]);
2453
2454 let candidates = device_aware_candidate_group(&state, &instance_ids, &device_type_map, 8);
2455 assert_eq!(candidates, vec![1, 2, 3]);
2456
2457 let selected = state
2458 .select_exact_min_and_increment(&candidates)
2459 .await
2460 .unwrap();
2461 assert_eq!(selected, 2);
2462 }
2463
2464 #[test]
2465 fn device_aware_group_uses_ratio_budget() {
2466 let state = RoutingOccupancyState::default();
2467 for _ in 0..4 {
2469 state.increment(3);
2470 state.increment(4);
2471 }
2472 for _ in 0..3 {
2474 state.increment(1);
2475 }
2476 let instance_ids = vec![1, 2, 3, 4];
2480 let device_type_map = HashMap::from([
2481 (1, Some(DeviceType::Cpu)),
2482 (2, Some(DeviceType::Cpu)),
2483 (3, Some(DeviceType::Cuda)),
2484 (4, Some(DeviceType::Cuda)),
2485 ]);
2486
2487 let candidates = device_aware_candidate_group(&state, &instance_ids, &device_type_map, 2);
2488 assert_eq!(candidates, vec![1, 2]);
2489
2490 let selected =
2492 futures::executor::block_on(state.select_exact_min_and_increment(&candidates)).unwrap();
2493 assert_eq!(selected, 2);
2494 }
2495
2496 #[tokio::test]
2497 async fn device_aware_weighted_peek_returns_available_worker_select_stays_none() {
2498 let rt = Runtime::from_current().unwrap();
2499 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2500 .await
2501 .unwrap();
2502 let ns = drt
2503 .namespace("test_device_aware_router".to_string())
2504 .unwrap();
2505 let component = ns.component("test_component".to_string()).unwrap();
2506 let endpoint = component.endpoint("test_endpoint".to_string());
2507 let client = endpoint.client().await.unwrap();
2508
2509 endpoint.register_endpoint_instance().await.unwrap();
2510 client.wait_for_instances().await.unwrap();
2511
2512 let router =
2513 PushRouter::<u64, TestResponse>::from_client(client, RouterMode::DeviceAwareWeighted)
2514 .await
2515 .unwrap();
2516
2517 assert_eq!(router.select_next_worker(), None);
2520 assert!(
2521 router.peek_next_worker().is_some(),
2522 "DeviceAwareWeighted peek must return the available worker for disagg bootstrap"
2523 );
2524
2525 rt.shutdown();
2526 }
2527
2528 #[tokio::test]
2529 async fn device_aware_exact_selection_preserves_full_multimodal_cache_hit() {
2530 let rt = Runtime::from_current().unwrap();
2531 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2532 .await
2533 .unwrap();
2534 let ns = drt
2535 .namespace("test_device_aware_affinity_cache".to_string())
2536 .unwrap();
2537 let component = ns.component("test_component".to_string()).unwrap();
2538 let endpoint = component.endpoint("test_endpoint".to_string());
2539 let client = endpoint.client().await.unwrap();
2540 endpoint.register_endpoint_instance().await.unwrap();
2541 let cache_worker = client.wait_for_instances().await.unwrap()[0].id();
2542
2543 let router = PushRouter::<u64, TestResponse>::from_client_with_state(
2544 client,
2545 RouterMode::DeviceAwareWeighted,
2546 None,
2547 Some(Arc::new(StaticMultimodalCacheIndex {
2548 worker_id: cache_worker,
2549 })),
2550 Some(Arc::new(|_| vec!["image-key".to_string()])),
2551 )
2552 .await
2553 .unwrap();
2554
2555 let (worker_id, permit) = router.select_exact_target(&42, None).await.unwrap();
2556 assert_eq!(worker_id, cache_worker);
2557 assert!(
2558 permit.is_none(),
2559 "full cache hits bypass occupancy charging"
2560 );
2561
2562 rt.shutdown();
2563 }
2564
2565 #[tokio::test]
2567 async fn direct_dispatch_ignores_local_inhibition() {
2568 let rt = Runtime::from_current().unwrap();
2569 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2570 .await
2571 .unwrap();
2572 let ns = drt
2573 .namespace("test_direct_bypasses_inhibition".to_string())
2574 .unwrap();
2575 let component = ns.component("test_component".to_string()).unwrap();
2576 let endpoint = component.endpoint("test_endpoint".to_string());
2577 let client = endpoint.client().await.unwrap();
2578 endpoint.register_endpoint_instance().await.unwrap();
2579 let instance_id = client.wait_for_instances().await.unwrap()[0].id();
2580
2581 let router = PushRouter::<u64, TestResponse>::from_client(client.clone(), RouterMode::KV)
2583 .await
2584 .unwrap();
2585
2586 client.report_instance_down(instance_id);
2587 assert!(
2588 !client.instance_ids_avail().contains(&instance_id),
2589 "precondition: worker should be locally inhibited"
2590 );
2591
2592 let result = router
2593 .direct_within_prepared(
2594 SingleIn::new(42),
2595 instance_id,
2596 None,
2597 |_, selected_instance_id| {
2598 assert_eq!(selected_instance_id, instance_id);
2599 Err::<(), _>(anyhow::anyhow!("direct prepare sentinel"))
2600 },
2601 )
2602 .await;
2603 let error = match result {
2604 Ok(_) => panic!("direct dispatch should reach request preparation"),
2605 Err(error) => error,
2606 };
2607 assert_eq!(error.to_string(), "direct prepare sentinel");
2608
2609 let missing_instance_id = instance_id.wrapping_add(1);
2610 let result = router
2611 .direct_within_prepared(SingleIn::new(42), missing_instance_id, None, |_, _| {
2612 Ok::<(), anyhow::Error>(())
2613 })
2614 .await;
2615 let error = match result {
2616 Ok(_) => panic!("direct dispatch should reject a worker absent from discovery"),
2617 Err(error) => error,
2618 };
2619 assert!(
2620 error
2621 .to_string()
2622 .contains(&format!("instance_id={missing_instance_id} not found")),
2623 "unexpected missing-worker error: {error}"
2624 );
2625
2626 rt.shutdown();
2627 }
2628
2629 #[tokio::test]
2633 async fn transport_resolution_falls_back_when_selected_instance_disappears() {
2634 let rt = Runtime::from_current().unwrap();
2635 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2636 .await
2637 .unwrap();
2638 let ns = drt
2639 .namespace("test_transport_fallback".to_string())
2640 .unwrap();
2641 let component = ns.component("test_component".to_string()).unwrap();
2642 let endpoint = component.endpoint("test_endpoint".to_string());
2643 let client = endpoint.client().await.unwrap();
2644
2645 endpoint.register_endpoint_instance().await.unwrap();
2647 client.wait_for_instances().await.unwrap();
2648
2649 let real_id = client.instance_ids()[0];
2650
2651 let stale_id = real_id + 1000;
2655 client.override_instance_avail(vec![stale_id, real_id]);
2656
2657 let router =
2658 PushRouter::<u64, TestResponse>::from_client(client.clone(), RouterMode::RoundRobin)
2659 .await
2660 .unwrap();
2661
2662 let (resolved_id, _, _, _) = router
2666 .resolve_transport(stale_id, TransportFallback::Allow)
2667 .expect("normal routing should fall back from a stale worker");
2668 assert_eq!(resolved_id, real_id);
2669
2670 rt.shutdown();
2671 }
2672
2673 #[tokio::test]
2674 async fn prepared_dispatch_observes_worker_after_transport_fallback() {
2675 let rt = Runtime::from_current().unwrap();
2676 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2677 .await
2678 .unwrap();
2679 let endpoint = drt
2680 .namespace("test_prepared_transport_fallback".to_string())
2681 .unwrap()
2682 .component("test_component".to_string())
2683 .unwrap()
2684 .endpoint("test_endpoint".to_string());
2685 let client = endpoint.client().await.unwrap();
2686 endpoint.register_endpoint_instance().await.unwrap();
2687 let real_id = client.wait_for_instances().await.unwrap()[0].id();
2688 let stale_id = real_id.wrapping_add(1);
2689 client.override_instance_avail(vec![stale_id, real_id]);
2690 let router = PushRouter::<u64, TestResponse>::from_client(client, RouterMode::LeastLoaded)
2691 .await
2692 .unwrap();
2693 let state = router.occupancy_state.clone().unwrap();
2694 state.increment(real_id);
2695 let state_for_prepare = state.clone();
2696 let observed = Arc::new(AtomicU64::new(0));
2697 let observed_for_prepare = observed.clone();
2698
2699 let _ = tokio::time::timeout(
2700 std::time::Duration::from_millis(100),
2701 router.select_and_dispatch(SingleIn::new(42), move |_, worker_id| {
2702 assert_eq!(state_for_prepare.load(stale_id), 0);
2703 assert_eq!(state_for_prepare.load(worker_id), 2);
2704 observed_for_prepare.store(worker_id, Ordering::Relaxed);
2705 Ok(())
2706 }),
2707 )
2708 .await;
2709
2710 assert_eq!(observed.load(Ordering::Relaxed), real_id);
2711 assert_eq!(state.load(real_id), 1);
2712 state.decrement(real_id);
2713 rt.shutdown();
2714 }
2715
2716 #[tokio::test]
2719 async fn transport_resolution_errors_when_no_instances_available() {
2720 let rt = Runtime::from_current().unwrap();
2721 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2722 .await
2723 .unwrap();
2724 let ns = drt
2725 .namespace("test_transport_no_fallback".to_string())
2726 .unwrap();
2727 let component = ns.component("test_component".to_string()).unwrap();
2728 let endpoint = component.endpoint("test_endpoint".to_string());
2729 let client = endpoint.client().await.unwrap();
2730
2731 endpoint.register_endpoint_instance().await.unwrap();
2733 client.wait_for_instances().await.unwrap();
2734
2735 let router =
2736 PushRouter::<u64, TestResponse>::from_client(client.clone(), RouterMode::RoundRobin)
2737 .await
2738 .unwrap();
2739
2740 let stale_id = 99999;
2743 client.override_instance_avail(vec![stale_id]);
2744
2745 let request = SingleIn::new(42u64);
2746 let result = router.generate(request).await;
2747
2748 assert!(result.is_err());
2749 let error = result.unwrap_err();
2750 assert_not_cannot_connect(&error);
2751 let msg = error.to_string();
2752 assert!(
2753 msg.contains("not found") && msg.contains("no other instances available"),
2754 "Expected clear error about missing instance with no fallback, got: {msg}"
2755 );
2756
2757 rt.shutdown();
2758 }
2759
2760 #[tokio::test]
2761 async fn transport_resolution_honors_fallback_policy() {
2762 let rt = Runtime::from_current().unwrap();
2763 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2764 .await
2765 .unwrap();
2766 let ns = drt
2767 .namespace("test_exact_transport_no_fallback".to_string())
2768 .unwrap();
2769 let component = ns.component("test_component".to_string()).unwrap();
2770 let endpoint = component.endpoint("test_endpoint".to_string());
2771 let client = endpoint.client().await.unwrap();
2772 endpoint.register_endpoint_instance().await.unwrap();
2773 let instances = client.wait_for_instances().await.unwrap();
2774 let real_id = instances[0].id();
2775
2776 let router =
2777 PushRouter::<u64, TestResponse>::from_client(client.clone(), RouterMode::RoundRobin)
2778 .await
2779 .unwrap();
2780 let stale_id = real_id.wrapping_add(1);
2781 client.override_instance_avail(vec![stale_id, real_id]);
2782
2783 assert!(
2784 router
2785 .resolve_transport(stale_id, TransportFallback::Allow)
2786 .is_ok(),
2787 "normal dispatch should preserve transport fallback"
2788 );
2789 let allowed = HashSet::from([real_id]);
2790 assert!(
2791 router
2792 .resolve_transport(stale_id, TransportFallback::Within(&allowed))
2793 .is_ok(),
2794 "constrained dispatch should fall back within the allowed worker set"
2795 );
2796 let disallowed = HashSet::new();
2797 let disallowed_error = router
2798 .resolve_transport(stale_id, TransportFallback::Within(&disallowed))
2799 .unwrap_err();
2800 assert_not_cannot_connect(&disallowed_error);
2801
2802 let exact_error = router
2803 .resolve_transport(stale_id, TransportFallback::Deny)
2804 .unwrap_err();
2805 assert_cannot_connect(&exact_error);
2806
2807 let second_stale_id = stale_id.wrapping_add(1);
2808 client.override_instance_avail(vec![stale_id, second_stale_id]);
2809 let stale_fallback_error = router
2810 .resolve_transport(stale_id, TransportFallback::Allow)
2811 .unwrap_err();
2812 assert_cannot_connect(&stale_fallback_error);
2813 assert!(
2814 stale_fallback_error
2815 .to_string()
2816 .contains("Fallback instance"),
2817 "expected fallback lookup failure, got: {stale_fallback_error}"
2818 );
2819
2820 rt.shutdown();
2821 }
2822
2823 #[tokio::test]
2835 async fn watcher_dedup_guard_released_on_panic() {
2836 let endpoint_id = EndpointId {
2837 namespace: "panic-test-ns".to_string(),
2838 component: "panic-test-comp".to_string(),
2839 name: "panic-test-endpoint".to_string(),
2840 };
2841
2842 let map = ENDPOINT_WATCHER_ACTIVE.get_or_init(dashmap::DashMap::new);
2844 map.insert(endpoint_id.clone(), ());
2845
2846 let endpoint_id_clone = endpoint_id.clone();
2847 let join = tokio::spawn(async move {
2848 struct GuardRelease(EndpointId);
2850 impl Drop for GuardRelease {
2851 fn drop(&mut self) {
2852 if let Some(map) = ENDPOINT_WATCHER_ACTIVE.get() {
2853 map.remove(&self.0);
2854 }
2855 }
2856 }
2857 let _release = GuardRelease(endpoint_id_clone);
2858 panic!("simulated watcher-task panic");
2859 });
2860
2861 let result = join.await;
2862 assert!(result.is_err() && result.unwrap_err().is_panic());
2863 assert!(
2864 !map.contains_key(&endpoint_id),
2865 "Drop guard must release the dedup entry even on panic"
2866 );
2867 }
2868
2869 #[tokio::test]
2873 async fn watcher_dedup_guard_released_on_normal_exit() {
2874 let endpoint_id = EndpointId {
2875 namespace: "normal-test-ns".to_string(),
2876 component: "normal-test-comp".to_string(),
2877 name: "normal-test-endpoint".to_string(),
2878 };
2879
2880 let map = ENDPOINT_WATCHER_ACTIVE.get_or_init(dashmap::DashMap::new);
2881 map.insert(endpoint_id.clone(), ());
2882
2883 let endpoint_id_clone = endpoint_id.clone();
2884 tokio::spawn(async move {
2885 struct GuardRelease(EndpointId);
2886 impl Drop for GuardRelease {
2887 fn drop(&mut self) {
2888 if let Some(map) = ENDPOINT_WATCHER_ACTIVE.get() {
2889 map.remove(&self.0);
2890 }
2891 }
2892 }
2893 let _release = GuardRelease(endpoint_id_clone);
2894 })
2896 .await
2897 .unwrap();
2898
2899 assert!(!map.contains_key(&endpoint_id));
2900 }
2901}