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 self.direct_within(request, instance_id, None).await
786 }
787
788 pub async fn direct_within(
793 &self,
794 request: SingleIn<T>,
795 instance_id: u64,
796 allowed_fallback: Option<&HashSet<u64>>,
797 ) -> anyhow::Result<ManyOut<U>> {
798 self.direct_within_prepared(request, instance_id, allowed_fallback, |_, _| Ok(()))
799 .await
800 .map(|(_, stream)| stream)
801 }
802
803 pub async fn direct_within_prepared<M, F>(
806 &self,
807 request: SingleIn<T>,
808 instance_id: u64,
809 allowed_fallback: Option<&HashSet<u64>>,
810 prepare: F,
811 ) -> anyhow::Result<(M, ManyOut<U>)>
812 where
813 F: FnOnce(&mut T, u64) -> anyhow::Result<M>,
814 {
815 let found = {
819 if self.fault_detection_enabled {
820 let routing_instances = self.client.routing_instances();
821 routing_instances.routable_ids().contains(&instance_id)
822 } else {
823 self.client.instance_ids().contains(&instance_id)
824 }
825 };
826
827 if !found {
828 return Err(DynamoError::builder()
829 .error_type(ErrorType::CannotConnect)
830 .message(format!(
831 "instance_id={instance_id} not found for endpoint {}",
832 self.client.endpoint.id()
833 ))
834 .build()
835 .into());
836 }
837
838 tracing::info!(
839 router_mode = "direct",
840 worker_id = instance_id,
841 "Selected worker"
842 );
843
844 let fallback = allowed_fallback
845 .map(TransportFallback::Within)
846 .unwrap_or(TransportFallback::Allow);
847 self.generate_with_fault_detection_prepared(instance_id, request, fallback, prepare)
848 .await
849 }
850
851 pub async fn dispatch_exact(
856 &self,
857 request: SingleIn<T>,
858 instance_id: u64,
859 ) -> anyhow::Result<ManyOut<U>> {
860 self.generate_with_fault_detection(instance_id, request, TransportFallback::Deny)
861 .await
862 }
863
864 pub async fn select_and_dispatch_exact<M, F>(
867 &self,
868 mut request: SingleIn<T>,
869 pinned_worker: Option<u64>,
870 prepare: F,
871 ) -> anyhow::Result<(M, ManyOut<U>)>
872 where
873 F: FnOnce(&mut T, u64) -> anyhow::Result<M>,
874 {
875 let (instance_id, permit) = self
876 .select_exact_target(request.content(), pinned_worker)
877 .await?;
878 let metadata = prepare(&mut request, instance_id)?;
879 let stream = self.dispatch_exact(request, instance_id).await?;
880 let stream = match permit {
881 Some(permit) => permit.into_tracked_stream(stream),
882 None => stream,
883 };
884 Ok((metadata, stream))
885 }
886
887 pub async fn select_and_dispatch<M, F>(
890 &self,
891 request: SingleIn<T>,
892 prepare: F,
893 ) -> anyhow::Result<(M, ManyOut<U>)>
894 where
895 F: FnOnce(&mut T, u64) -> anyhow::Result<M>,
896 {
897 match self.router_mode {
898 RouterMode::Random => self.random_prepared(request, prepare).await,
899 RouterMode::RoundRobin => self.round_robin_prepared(request, prepare).await,
900 RouterMode::PowerOfTwoChoices => {
901 self.power_of_two_choices_prepared(request, prepare).await
902 }
903 RouterMode::LeastLoaded => self.least_loaded_prepared(request, prepare).await,
904 RouterMode::DeviceAwareWeighted => {
905 self.device_aware_weighted_prepared(request, prepare).await
906 }
907 RouterMode::KV => anyhow::bail!("KV routing should not call select_and_dispatch"),
908 RouterMode::Direct => anyhow::bail!(
909 "Direct routing should use direct_within_prepared instead of select_and_dispatch"
910 ),
911 }
912 }
913
914 async fn dispatch_selected<M, F>(
915 &self,
916 instance_id: u64,
917 request: SingleIn<T>,
918 mut permit: Option<OccupancyPermit>,
919 prepare: F,
920 ) -> anyhow::Result<(M, ManyOut<U>)>
921 where
922 F: FnOnce(&mut T, u64) -> anyhow::Result<M>,
923 {
924 let (metadata, stream) = self
925 .generate_with_fault_detection_prepared(
926 instance_id,
927 request,
928 TransportFallback::Allow,
929 |request, resolved_instance_id| {
930 if let Some(permit) = permit.as_mut() {
931 permit.retarget(resolved_instance_id);
932 }
933 prepare(request, resolved_instance_id)
934 },
935 )
936 .await?;
937 let stream = match permit {
938 Some(permit) => permit.into_tracked_stream(stream),
939 None => stream,
940 };
941 Ok((metadata, stream))
942 }
943
944 pub async fn device_aware_weighted(&self, request: SingleIn<T>) -> anyhow::Result<ManyOut<U>> {
953 self.device_aware_weighted_prepared(request, |_, _| Ok(()))
954 .await
955 .map(|(_, stream)| stream)
956 }
957
958 async fn device_aware_weighted_prepared<M, F>(
959 &self,
960 request: SingleIn<T>,
961 prepare: F,
962 ) -> anyhow::Result<(M, ManyOut<U>)>
963 where
964 F: FnOnce(&mut T, u64) -> anyhow::Result<M>,
965 {
966 let state = self.occupancy_state()?;
967 let routing_instances = self.client.routing_instances();
968 let instance_ids = routing_instances.free_ids().to_vec();
969
970 if instance_ids.is_empty() {
971 return Err(self.empty_free_pool_error(&routing_instances));
972 }
973
974 let endpoint_id = self.client.endpoint.id();
976
977 let selection =
978 self.device_aware_candidates(request.content(), state.as_ref(), &instance_ids);
979
980 let instance_id = if selection.full_embedding_cache_hit {
983 state.select_exact_min(&selection.candidates).await
984 } else {
985 state
986 .select_exact_min_and_increment(&selection.candidates)
987 .await
988 }
989 .ok_or_else(|| self.empty_free_pool_error(&routing_instances))?;
990 let permit = if selection.full_embedding_cache_hit {
991 None
992 } else {
993 Some(OccupancyPermit::new(state.clone(), instance_id))
994 };
995 let is_cpu = matches!(
996 selection.device_type_map.get(&instance_id),
997 Some(Some(DeviceType::Cpu))
998 );
999 tracing::info!(
1000 router_mode = "device-aware-weighted",
1001 worker_id = instance_id,
1002 candidate_count = selection.candidates.len(),
1003 load = state.load(instance_id),
1004 endpoint = %endpoint_id,
1005 is_cpu,
1006 embedding_cache_hit = selection.embedding_cache_hit,
1007 request_cache_keys = selection.request_cache_keys,
1008 "Selected worker"
1009 );
1010
1011 self.dispatch_selected(instance_id, request, permit, prepare)
1012 .await
1013 }
1014
1015 fn device_aware_candidates(
1016 &self,
1017 request: &T,
1018 state: &RoutingOccupancyState,
1019 instance_ids: &[u64],
1020 ) -> DeviceAwareCandidates {
1021 let device_type_map = self
1022 .client
1023 .instances()
1024 .iter()
1025 .map(|instance| (instance.instance_id, instance.device_type.clone()))
1026 .collect();
1027 let cuda_to_cpu_ratio = std::env::var("DYN_ENCODER_CUDA_TO_CPU_RATIO")
1028 .ok()
1029 .and_then(|value| value.parse::<usize>().ok())
1030 .filter(|value| *value >= 1)
1031 .unwrap_or(8);
1032
1033 let (request_cache_keys, cache_matched_candidates) =
1034 if let (Some(indexer), Some(extractor)) = (
1035 self.multimodal_cache_indexer.as_ref(),
1036 self.multimodal_cache_key_extractor.as_ref(),
1037 ) {
1038 let request_cache_keys = extractor(request);
1039 let matched = if request_cache_keys.is_empty() {
1040 Vec::new()
1041 } else {
1042 let mut matched = indexer.workers_with_cache_key_hits(&request_cache_keys);
1043 matched.retain(|(id, _)| instance_ids.contains(id));
1044 matched
1045 };
1046 (request_cache_keys, matched)
1047 } else {
1048 (Vec::new(), Vec::new())
1049 };
1050
1051 let embedding_cache_hit = !cache_matched_candidates.is_empty();
1052 let request_cache_key_count = request_cache_keys
1053 .iter()
1054 .collect::<std::collections::HashSet<_>>()
1055 .len();
1056 let full_cache_candidates = cache_matched_candidates
1057 .iter()
1058 .filter_map(|(worker_id, hits)| {
1059 (*hits >= request_cache_key_count).then_some(*worker_id)
1060 })
1061 .collect::<Vec<_>>();
1062 let full_embedding_cache_hit = !full_cache_candidates.is_empty();
1063 let candidates = if full_embedding_cache_hit {
1064 full_cache_candidates
1065 } else {
1066 device_aware_candidate_group(state, instance_ids, &device_type_map, cuda_to_cpu_ratio)
1067 };
1068
1069 DeviceAwareCandidates {
1070 candidates,
1071 device_type_map,
1072 embedding_cache_hit,
1073 full_embedding_cache_hit,
1074 request_cache_keys: request_cache_keys.len(),
1075 }
1076 }
1077
1078 pub async fn least_loaded(&self, request: SingleIn<T>) -> anyhow::Result<ManyOut<U>> {
1080 self.least_loaded_prepared(request, |_, _| Ok(()))
1081 .await
1082 .map(|(_, stream)| stream)
1083 }
1084
1085 async fn least_loaded_prepared<M, F>(
1086 &self,
1087 request: SingleIn<T>,
1088 prepare: F,
1089 ) -> anyhow::Result<(M, ManyOut<U>)>
1090 where
1091 F: FnOnce(&mut T, u64) -> anyhow::Result<M>,
1092 {
1093 let state = self.occupancy_state()?;
1094 let routing_instances = self.client.routing_instances();
1095 let instance_ids = routing_instances.free_ids().to_vec();
1096 let instance_id = state
1097 .select_exact_min_and_increment(&instance_ids)
1098 .await
1099 .ok_or_else(|| self.empty_free_pool_error(&routing_instances))?;
1100 let permit = OccupancyPermit::new(state.clone(), instance_id);
1101 tracing::info!(
1102 router_mode = "least-loaded",
1103 worker_id = instance_id,
1104 candidate_count = instance_ids.len(),
1105 load = state.load(instance_id),
1106 "Selected worker"
1107 );
1108
1109 self.dispatch_selected(instance_id, request, Some(permit), prepare)
1110 .await
1111 }
1112
1113 pub fn select_next_worker(&self) -> Option<u64> {
1117 let routing_instances = self.client.routing_instances();
1118 let count = routing_instances.free_ids().len();
1119 if count == 0 {
1120 return None;
1121 }
1122
1123 match self.router_mode {
1124 RouterMode::RoundRobin => {
1125 let counter = self.round_robin_counter.fetch_add(1, Ordering::Relaxed) as usize;
1126 Some(routing_instances.free_ids()[counter % count])
1127 }
1128 RouterMode::Random => {
1129 let counter = rand::rng().random::<u64>() as usize;
1130 Some(routing_instances.free_ids()[counter % count])
1131 }
1132 RouterMode::PowerOfTwoChoices
1133 | RouterMode::Direct
1134 | RouterMode::LeastLoaded
1135 | RouterMode::DeviceAwareWeighted => None,
1136 RouterMode::KV => {
1137 panic!(
1138 "select_next_worker should not be called for {:?} routing mode",
1139 self.router_mode
1140 )
1141 }
1142 }
1143 }
1144
1145 pub fn peek_next_worker(&self) -> Option<u64> {
1151 let instance_ids = self.client.routing_instances().free_ids().to_vec();
1154 let count = instance_ids.len();
1155 if count == 0 {
1156 return None;
1157 }
1158
1159 match self.router_mode {
1160 RouterMode::RoundRobin => {
1161 let counter = self.round_robin_counter.load(Ordering::Relaxed) as usize;
1163 Some(instance_ids[counter % count])
1164 }
1165 RouterMode::Random => {
1166 let counter = rand::rng().random::<u64>() as usize;
1169 Some(instance_ids[counter % count])
1170 }
1171 RouterMode::LeastLoaded => self.occupancy_state.as_deref()?.peek_min(&instance_ids),
1172 RouterMode::PowerOfTwoChoices => Some(p2c_select_from(
1173 self.occupancy_state.as_deref()?,
1174 &instance_ids,
1175 )),
1176 RouterMode::DeviceAwareWeighted => {
1177 let state = self.occupancy_state.as_deref()?;
1178 let device_type_map: HashMap<u64, Option<DeviceType>> = self
1179 .client
1180 .instances()
1181 .iter()
1182 .map(|instance| (instance.instance_id, instance.device_type.clone()))
1183 .collect();
1184 let cuda_to_cpu_ratio = std::env::var("DYN_ENCODER_CUDA_TO_CPU_RATIO")
1185 .ok()
1186 .and_then(|value| value.parse::<usize>().ok())
1187 .filter(|value| *value >= 1)
1188 .unwrap_or(8);
1189 let candidates = device_aware_candidate_group(
1190 state,
1191 &instance_ids,
1192 &device_type_map,
1193 cuda_to_cpu_ratio,
1194 );
1195 state.peek_min(&candidates)
1196 }
1197 RouterMode::Direct => None,
1198 RouterMode::KV => {
1199 panic!(
1200 "peek_next_worker should not be called for {:?} routing mode",
1201 self.router_mode
1202 )
1203 }
1204 }
1205 }
1206
1207 async fn select_exact_target(
1208 &self,
1209 request: &T,
1210 pinned_worker: Option<u64>,
1211 ) -> anyhow::Result<(u64, Option<OccupancyPermit>)> {
1212 if let Some(instance_id) = pinned_worker {
1213 let routing_instances = self.client.routing_instances();
1214 if !routing_instances.routable_ids().contains(&instance_id) {
1215 return Err(anyhow::anyhow!(
1216 "instance_id={instance_id} not found for endpoint {}",
1217 self.client.endpoint.id()
1218 ));
1219 }
1220 let permit = match self.router_mode {
1221 RouterMode::LeastLoaded
1222 | RouterMode::PowerOfTwoChoices
1223 | RouterMode::DeviceAwareWeighted => {
1224 let state = self.occupancy_state()?;
1225 state.increment(instance_id);
1226 Some(OccupancyPermit::new(state, instance_id))
1227 }
1228 RouterMode::RoundRobin
1229 | RouterMode::Random
1230 | RouterMode::Direct
1231 | RouterMode::KV => None,
1232 };
1233 return Ok((instance_id, permit));
1234 }
1235
1236 match self.router_mode {
1237 RouterMode::LeastLoaded
1238 | RouterMode::PowerOfTwoChoices
1239 | RouterMode::DeviceAwareWeighted => {
1240 let state = self.occupancy_state()?;
1241 let routing_instances = self.client.routing_instances();
1242 let instance_ids = routing_instances.free_ids().to_vec();
1243 if instance_ids.is_empty() {
1244 return Err(self.empty_free_pool_error(&routing_instances));
1245 }
1246
1247 let instance_id = match self.router_mode {
1248 RouterMode::LeastLoaded => state
1249 .select_exact_min_and_increment(&instance_ids)
1250 .await
1251 .ok_or_else(|| self.empty_free_pool_error(&routing_instances))?,
1252 RouterMode::PowerOfTwoChoices => {
1253 let instance_id = p2c_select_from(state.as_ref(), &instance_ids);
1254 state.increment(instance_id);
1255 instance_id
1256 }
1257 RouterMode::DeviceAwareWeighted => {
1258 let selection =
1259 self.device_aware_candidates(request, state.as_ref(), &instance_ids);
1260 let instance_id = if selection.full_embedding_cache_hit {
1261 state.select_exact_min(&selection.candidates).await
1262 } else {
1263 state
1264 .select_exact_min_and_increment(&selection.candidates)
1265 .await
1266 }
1267 .ok_or_else(|| self.empty_free_pool_error(&routing_instances))?;
1268 let permit = (!selection.full_embedding_cache_hit)
1269 .then(|| OccupancyPermit::new(state, instance_id));
1270 return Ok((instance_id, permit));
1271 }
1272 _ => unreachable!(),
1273 };
1274 Ok((instance_id, Some(OccupancyPermit::new(state, instance_id))))
1275 }
1276 RouterMode::RoundRobin | RouterMode::Random => self
1277 .select_next_worker()
1278 .map(|instance_id| (instance_id, None))
1279 .ok_or_else(|| {
1280 let routing_instances = self.client.routing_instances();
1281 self.empty_free_pool_error(&routing_instances)
1282 }),
1283 RouterMode::Direct => Err(anyhow::anyhow!(
1284 "Worker ID required for exact dispatch in Direct routing mode"
1285 )),
1286 RouterMode::KV => Err(anyhow::anyhow!(
1287 "select_and_dispatch_exact cannot select workers in KV routing mode"
1288 )),
1289 }
1290 }
1291
1292 fn occupancy_state(&self) -> anyhow::Result<Arc<RoutingOccupancyState>> {
1293 self.occupancy_state.clone().ok_or_else(|| {
1294 anyhow::anyhow!(
1295 "routing occupancy state not initialized for endpoint {}",
1296 self.client.endpoint.id()
1297 )
1298 })
1299 }
1300
1301 async fn generate_with_fault_detection(
1312 &self,
1313 instance_id: u64,
1314 request: SingleIn<T>,
1315 fallback: TransportFallback<'_>,
1316 ) -> anyhow::Result<ManyOut<U>> {
1317 self.generate_with_fault_detection_prepared(instance_id, request, fallback, |_, _| Ok(()))
1318 .await
1319 .map(|(_, stream)| stream)
1320 }
1321
1322 async fn generate_with_fault_detection_prepared<M, F>(
1323 &self,
1324 instance_id: u64,
1325 mut request: SingleIn<T>,
1326 fallback: TransportFallback<'_>,
1327 prepare: F,
1328 ) -> anyhow::Result<(M, ManyOut<U>)>
1329 where
1330 F: FnOnce(&mut T, u64) -> anyhow::Result<M>,
1331 {
1332 let route_start = Instant::now();
1333 let request_id = request.id().to_string();
1334 let route_span = if matches!(self.router_mode, RouterMode::KV) {
1335 tracing::Span::none()
1336 } else {
1337 tracing::info_span!(
1338 "router.route_request",
1339 request_id = %request_id,
1340 worker_id = instance_id,
1341 router_mode = ?self.router_mode,
1342 )
1343 };
1344
1345 self.check_workers_available(instance_id, &request_id)?;
1346
1347 let (instance_id, address, transport_kind, instance) =
1348 self.resolve_transport(instance_id, fallback)?;
1349 let metadata = prepare(&mut request, instance_id)?;
1350 let request = request.map(|req| AddressedRequest::with_instance(req, address, instance));
1351
1352 STAGE_DURATION_SECONDS
1353 .with_label_values(&[STAGE_ROUTE])
1354 .observe(route_start.elapsed().as_secs_f64());
1355
1356 let _nvtx_transport = dynamo_nvtx_range!(transport_kind);
1357 let stream = self
1358 .addressed
1359 .generate(request)
1360 .instrument(route_span)
1361 .await;
1362 let stream = self.wrap_with_fault_detection(stream, instance_id)?;
1363 Ok((metadata, stream))
1364 }
1365
1366 fn check_workers_available(&self, instance_id: u64, request_id: &str) -> anyhow::Result<()> {
1371 if !self.fault_detection_enabled {
1372 return Ok(());
1373 }
1374 let routing_instances = self.client.routing_instances();
1375 let selected_worker_overloaded = routing_instances.is_overloaded(instance_id);
1376 let counts = routing_instances.counts();
1377 if tracing::enabled!(tracing::Level::DEBUG) {
1378 tracing::debug!(
1379 request_id,
1380 instance_id,
1381 router_mode = ?self.router_mode,
1382 free_workers = counts.free,
1383 overloaded_workers = counts.overloaded,
1384 total_workers = counts.discovered,
1385 selected_worker_overloaded,
1386 "checked worker overload state"
1387 );
1388 }
1389 if !selected_worker_overloaded {
1390 return Ok(());
1391 }
1392 tracing::warn!(
1393 instance_id,
1394 overloaded_workers = counts.overloaded,
1395 total_workers = counts.discovered,
1396 "Rejecting request: selected worker is overloaded"
1397 );
1398 let cause = PipelineError::ServiceOverloaded(
1399 "Selected worker is overloaded, please retry later".into(),
1400 );
1401 Err(DynamoError::builder()
1402 .error_type(ErrorType::ResourceExhausted)
1403 .message("Selected worker is overloaded, please retry later")
1404 .cause(cause)
1405 .build()
1406 .into())
1407 }
1408
1409 fn resolve_transport(
1415 &self,
1416 instance_id: u64,
1417 fallback: TransportFallback<'_>,
1418 ) -> anyhow::Result<(u64, String, &'static str, Instance)> {
1419 use crate::component::TransportType;
1420
1421 let lookup = |id: u64| {
1422 self.client
1423 .instances()
1424 .iter()
1425 .find(|i| i.instance_id == id)
1426 .map(|instance| {
1427 let (addr, kind) = match &instance.transport {
1428 TransportType::Tcp(tcp_endpoint) => {
1429 (tcp_endpoint.clone(), "transport.tcp.request")
1430 }
1431 TransportType::Nats(subject) => (subject.clone(), "transport.nats.request"),
1432 };
1433 (addr, kind, instance.clone())
1434 })
1435 };
1436
1437 if let Some((addr, kind, inst)) = lookup(instance_id) {
1438 return Ok((instance_id, addr, kind, inst));
1439 }
1440 let allowed_fallback = match fallback {
1441 TransportFallback::Allow => None,
1442 TransportFallback::Deny => {
1443 return Err(anyhow::anyhow!(
1444 "Instance {} not found for endpoint {}",
1445 instance_id,
1446 self.client.endpoint.id()
1447 ));
1448 }
1449 TransportFallback::Within(allowed) => Some(allowed),
1450 };
1451
1452 let routing_instances = self.client.routing_instances();
1453 let fallback_id = routing_instances.free_ids().iter().copied().find(|&id| {
1454 id != instance_id && allowed_fallback.is_none_or(|allowed| allowed.contains(&id))
1455 });
1456 match fallback_id {
1457 Some(id) => {
1458 tracing::warn!(
1459 original_instance = instance_id,
1460 fallback_instance = id,
1461 "Instance disappeared during routing, reselecting"
1462 );
1463 let (addr, kind, inst) = lookup(id).ok_or_else(|| {
1464 anyhow::anyhow!(
1465 "Fallback instance {} also not found for endpoint {}",
1466 id,
1467 self.client.endpoint.id()
1468 )
1469 })?;
1470 Ok((id, addr, kind, inst))
1471 }
1472 None => Err(anyhow::anyhow!(
1473 "Instance {} not found and no other instances available for endpoint {}",
1474 instance_id,
1475 self.client.endpoint.id()
1476 )),
1477 }
1478 }
1479
1480 fn wrap_with_fault_detection(
1485 &self,
1486 stream: anyhow::Result<ManyOut<U>>,
1487 instance_id: u64,
1488 ) -> anyhow::Result<ManyOut<U>> {
1489 let stream = match stream {
1490 Ok(stream) => stream,
1491 Err(err) => {
1492 if self.fault_detection_enabled {
1493 if is_inhibited(err.as_ref()) {
1494 tracing::debug!(
1495 "Reporting instance {instance_id} down due to error: {err}"
1496 );
1497 self.client.report_instance_down(instance_id);
1498 } else if match_error_chain(err.as_ref(), &[ErrorType::ResourceExhausted], &[])
1499 {
1500 tracing::debug!(
1506 "Marking instance {instance_id} overloaded due to backpressure: {err}"
1507 );
1508 self.client.mark_overloaded_immediate(instance_id);
1509 }
1510 }
1511 return Err(err);
1512 }
1513 };
1514
1515 if !self.fault_detection_enabled {
1516 return Ok(stream);
1517 }
1518
1519 let engine_ctx = stream.context();
1520 let client = self.client.clone();
1521 let client_for_timeout = self.client.clone();
1522 let stream = stream.map(move |res| {
1523 if let Some(err) = res.err()
1524 && is_inhibited(&err)
1525 {
1526 tracing::debug!(
1527 "Reporting instance {instance_id} down due to migratable error: {err}"
1528 );
1529 client.report_instance_down(instance_id);
1530 }
1531 res
1532 });
1533
1534 let stream: Pin<Box<dyn Stream<Item = U> + Send>> =
1535 if let Some(timeout) = self.response_timeout {
1536 Box::pin(async_stream::stream! {
1537 let mut inner = Box::pin(stream);
1538 loop {
1539 tokio::select! {
1540 biased;
1541 item = inner.next() => {
1542 match item {
1543 Some(item) => yield item,
1544 None => break,
1545 }
1546 }
1547 _ = tokio::time::sleep(timeout) => {
1548 tracing::warn!(
1549 instance_id,
1550 timeout_secs = timeout.as_secs(),
1551 "backend response inactivity timeout — quarantining worker"
1552 );
1553 client_for_timeout.report_instance_down(instance_id);
1554 yield U::from_err(
1555 crate::error::DynamoError::builder()
1556 .error_type(crate::error::ErrorType::ResponseTimeout)
1557 .message("backend response inactivity timeout")
1558 .build()
1559 );
1560 break;
1561 }
1562 }
1563 }
1564 })
1565 } else {
1566 Box::pin(stream)
1567 };
1568
1569 Ok(ResponseStream::new(stream, engine_ctx))
1570 }
1571}
1572
1573#[async_trait]
1574impl<T, U> AsyncEngine<SingleIn<T>, ManyOut<U>, Error> for PushRouter<T, U>
1575where
1576 T: Data + Serialize,
1577 U: Data + for<'de> Deserialize<'de> + MaybeError,
1578{
1579 async fn generate(&self, request: SingleIn<T>) -> Result<ManyOut<U>, Error> {
1580 match self.router_mode {
1581 RouterMode::Random => self.random(request).await,
1582 RouterMode::RoundRobin => self.round_robin(request).await,
1583 RouterMode::PowerOfTwoChoices => self.power_of_two_choices(request).await,
1584 RouterMode::KV => {
1585 anyhow::bail!("KV routing should not call generate on PushRouter");
1586 }
1587 RouterMode::Direct => {
1588 anyhow::bail!(
1589 "Direct routing should not call generate on PushRouter directly; use DirectRoutingRouter wrapper"
1590 );
1591 }
1592 RouterMode::LeastLoaded => self.least_loaded(request).await,
1593 RouterMode::DeviceAwareWeighted => self.device_aware_weighted(request).await,
1594 }
1595 }
1596}
1597
1598impl<T, U> PushRouter<T, U>
1599where
1600 T: Data + Serialize,
1601 U: Data + for<'de> Deserialize<'de> + MaybeError,
1602{
1603 async fn bidirectional_dispatch(
1605 &self,
1606 instance_id: u64,
1607 input: ManyIn<T>,
1608 ) -> anyhow::Result<ManyOut<U>> {
1609 let route_start = Instant::now();
1610 let request_id = input.context().id().to_string();
1611 let route_span = tracing::info_span!(
1612 "router.route_request_bidirectional",
1613 request_id = %request_id,
1614 worker_id = instance_id,
1615 router_mode = ?self.router_mode,
1616 );
1617
1618 self.check_workers_available(instance_id, &request_id)?;
1619 let (instance_id, address, transport_kind, instance) =
1620 self.resolve_transport(instance_id, TransportFallback::Allow)?;
1621
1622 STAGE_DURATION_SECONDS
1623 .with_label_values(&[STAGE_ROUTE])
1624 .observe(route_start.elapsed().as_secs_f64());
1625
1626 let _nvtx_transport = dynamo_nvtx_range!(transport_kind);
1627 let stream: anyhow::Result<ManyOut<U>> = self
1628 .addressed
1629 .generate_bidirectional(instance, address, input)
1630 .instrument(route_span)
1631 .await;
1632 self.wrap_with_fault_detection(stream, instance_id)
1633 }
1634}
1635
1636#[async_trait]
1650impl<T, U> AsyncEngine<ManyIn<T>, ManyOut<U>, Error> for PushRouter<T, U>
1651where
1652 T: Data + Serialize,
1653 U: Data + for<'de> Deserialize<'de> + MaybeError,
1654{
1655 async fn generate(&self, input: ManyIn<T>) -> Result<ManyOut<U>, Error> {
1656 match self.router_mode {
1657 RouterMode::KV => {
1658 anyhow::bail!("KV routing should not call generate on PushRouter");
1659 }
1660 RouterMode::Direct => {
1661 anyhow::bail!(
1662 "Direct routing should not call generate on PushRouter directly; use DirectRoutingRouter wrapper"
1663 );
1664 }
1665 RouterMode::PowerOfTwoChoices
1672 | RouterMode::LeastLoaded
1673 | RouterMode::DeviceAwareWeighted => {
1674 anyhow::bail!(
1675 "{:?} routing is not yet supported for bidirectional dispatch",
1676 self.router_mode
1677 );
1678 }
1679 RouterMode::RoundRobin | RouterMode::Random => {}
1680 }
1681
1682 let instance_id = self
1683 .select_next_worker()
1684 .ok_or_else(|| anyhow::anyhow!("no instances available for bidirectional routing"))?;
1685
1686 self.bidirectional_dispatch(instance_id, input).await
1687 }
1688}
1689
1690struct OccupancyTrackedStream<U: Data> {
1691 inner: ManyOut<U>,
1692 state: Arc<RoutingOccupancyState>,
1693 instance_id: u64,
1694 released: bool,
1695}
1696
1697impl<U: Data> Drop for OccupancyTrackedStream<U> {
1698 fn drop(&mut self) {
1699 if !self.released {
1700 self.state.decrement(self.instance_id);
1701 }
1702 }
1703}
1704
1705impl<U: Data> std::fmt::Debug for OccupancyTrackedStream<U> {
1706 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1707 f.debug_struct("OccupancyTrackedStream")
1708 .field("instance_id", &self.instance_id)
1709 .finish()
1710 }
1711}
1712
1713impl<U: Data> Stream for OccupancyTrackedStream<U> {
1714 type Item = U;
1715
1716 fn poll_next(
1717 mut self: Pin<&mut Self>,
1718 cx: &mut std::task::Context<'_>,
1719 ) -> Poll<Option<Self::Item>> {
1720 let poll = self.inner.as_mut().poll_next(cx);
1721 if matches!(poll, Poll::Ready(None)) && !self.released {
1722 self.state.decrement(self.instance_id);
1723 self.released = true;
1724 }
1725 poll
1726 }
1727}
1728
1729impl<U: Data> AsyncEngineContextProvider for OccupancyTrackedStream<U> {
1730 fn context(&self) -> Arc<dyn AsyncEngineContext> {
1731 self.inner.context()
1732 }
1733}
1734
1735impl<U: Data> crate::engine::AsyncEngineStream<U> for OccupancyTrackedStream<U> {}
1736
1737#[cfg(test)]
1738mod tests {
1739 use super::*;
1740 use crate::{
1741 DistributedRuntime, Runtime,
1742 distributed::DistributedConfig,
1743 error::DynamoError,
1744 pipeline::{
1745 RequestStream, ResponseStream,
1746 context::{Context, Controller},
1747 },
1748 };
1749 use serde::{Deserialize, Serialize};
1750
1751 #[derive(Clone, Debug, Deserialize, Serialize)]
1752 struct TestResponse {
1753 error: Option<DynamoError>,
1754 }
1755
1756 impl MaybeError for TestResponse {
1757 fn from_err(err: impl std::error::Error + 'static) -> Self {
1758 Self {
1759 error: Some(DynamoError::from(
1760 Box::new(err) as Box<dyn std::error::Error + 'static>
1761 )),
1762 }
1763 }
1764
1765 fn err(&self) -> Option<DynamoError> {
1766 self.error.clone()
1767 }
1768 }
1769
1770 struct StaticMultimodalCacheIndex {
1771 worker_id: u64,
1772 }
1773
1774 impl MultimodalCacheIndex for StaticMultimodalCacheIndex {
1775 fn workers_with_cache_key_hits(&self, cache_keys: &[String]) -> Vec<(u64, usize)> {
1776 vec![(self.worker_id, cache_keys.len())]
1777 }
1778
1779 fn remove_worker(&self, _worker_id: u64) {}
1780 }
1781
1782 #[test]
1783 fn p2c_selects_lower_load_worker() {
1784 let state = RoutingOccupancyState::default();
1785 for _ in 0..10 {
1786 state.increment(1);
1787 }
1788 state.increment(2);
1789
1790 let result = p2c_select_from(&state, &[1, 2]);
1792 assert_eq!(result, 2);
1793 }
1794
1795 #[test]
1796 fn p2c_selects_single_worker() {
1797 let state = RoutingOccupancyState::default();
1798 assert_eq!(p2c_select_from(&state, &[42]), 42);
1799 }
1800
1801 #[test]
1802 fn p2c_treats_missing_counts_as_zero() {
1803 let state = RoutingOccupancyState::default();
1804 for _ in 0..5 {
1805 state.increment(1);
1806 }
1807 let result = p2c_select_from(&state, &[1, 2]);
1809 assert_eq!(result, 2);
1810 }
1811
1812 #[test]
1813 fn p2c_returns_valid_worker_on_tie() {
1814 let state = RoutingOccupancyState::default();
1815 for _ in 0..3 {
1816 state.increment(1);
1817 state.increment(2);
1818 }
1819
1820 for _ in 0..100 {
1821 let result = p2c_select_from(&state, &[1, 2]);
1822 assert!(result == 1 || result == 2);
1823 }
1824 }
1825
1826 #[test]
1827 fn occupancy_permit_decrements_before_stream_creation() {
1828 let state = Arc::new(RoutingOccupancyState::default());
1829 state.increment(42);
1830 let permit = OccupancyPermit::new(state.clone(), 42);
1831 assert_eq!(state.load(42), 1);
1832 drop(permit);
1833 assert_eq!(state.load(42), 0);
1834 }
1835
1836 #[test]
1837 fn occupancy_tracked_stream_decrements_on_drop() {
1838 let state = Arc::new(RoutingOccupancyState::default());
1839 state.increment(7);
1840 let permit = OccupancyPermit::new(state.clone(), 7);
1841 let ctx: Arc<dyn AsyncEngineContext> = Arc::new(Controller::default());
1842 let stream = permit.into_tracked_stream(ResponseStream::new(
1843 Box::pin(tokio_stream::iter(vec![1u64])),
1844 ctx,
1845 ));
1846 assert_eq!(state.load(7), 1);
1847 drop(stream);
1848 assert_eq!(state.load(7), 0);
1849 }
1850
1851 #[tokio::test]
1852 async fn occupancy_tracked_stream_decrements_on_completion() {
1853 let state = Arc::new(RoutingOccupancyState::default());
1854 state.increment(7);
1855 let permit = OccupancyPermit::new(state.clone(), 7);
1856 let ctx: Arc<dyn AsyncEngineContext> = Arc::new(Controller::default());
1857 let mut stream = permit.into_tracked_stream(ResponseStream::new(
1858 Box::pin(tokio_stream::iter(vec![1u64])),
1859 ctx,
1860 ));
1861
1862 assert_eq!(stream.next().await, Some(1));
1863 assert_eq!(state.load(7), 1);
1864 assert_eq!(stream.next().await, None);
1865 assert_eq!(state.load(7), 0);
1866 drop(stream);
1867 assert_eq!(state.load(7), 0, "drop must not release twice after EOF");
1868 }
1869
1870 #[test]
1871 fn p2c_lifecycle_tracks_inflight_counts_with_shared_tracker() {
1872 let state = Arc::new(RoutingOccupancyState::default());
1873 let mut permits = Vec::new();
1874 for _ in 0..5 {
1875 let selected = p2c_select_from(&state, &[1, 2]);
1876 state.increment(selected);
1877 permits.push(OccupancyPermit::new(state.clone(), selected));
1878 }
1879
1880 let total = state.load(1) + state.load(2);
1881 assert_eq!(total, 5, "5 in-flight requests should be tracked");
1882
1883 drop(permits);
1884 let total = state.load(1) + state.load(2);
1885 assert_eq!(total, 0, "All guards dropped, counts should be 0");
1886 }
1887
1888 #[test]
1889 fn p2c_never_selects_dominated_worker() {
1890 let state = RoutingOccupancyState::default();
1891 for _ in 0..100 {
1892 state.increment(3);
1893 }
1894
1895 let mut selected = [0u32; 3];
1896 for _ in 0..1000 {
1897 let result = p2c_select_from(&state, &[1, 2, 3]);
1898 match result {
1899 1 => selected[0] += 1,
1900 2 => selected[1] += 1,
1901 3 => selected[2] += 1,
1902 _ => panic!("unexpected worker id"),
1903 }
1904 }
1905 assert_eq!(
1906 selected[2], 0,
1907 "Worker 3 (load=100) should never be selected against load=0 workers, but got {} times",
1908 selected[2]
1909 );
1910 }
1911
1912 #[tokio::test]
1913 async fn least_loaded_selects_exact_min_and_tracks_counts() {
1914 let state = Arc::new(RoutingOccupancyState::default());
1915 state.increment(1);
1916 state.increment(1);
1917 state.increment(2);
1918
1919 let selected = state
1920 .select_exact_min_and_increment(&[1, 2, 3])
1921 .await
1922 .unwrap();
1923 assert_eq!(selected, 3);
1924
1925 let permit = OccupancyPermit::new(state.clone(), selected);
1926 assert_eq!(state.load(selected), 1);
1927 drop(permit);
1928 assert_eq!(state.load(selected), 0);
1929 }
1930
1931 #[tokio::test]
1932 async fn bidirectional_generate_bails_with_no_instances() {
1933 let rt = Runtime::from_current().unwrap();
1934 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1935 .await
1936 .unwrap();
1937 let ns = drt.namespace("test_bidi_no_instances".to_string()).unwrap();
1938 let component = ns.component("test_component".to_string()).unwrap();
1939 let endpoint = component.endpoint("test_endpoint".to_string());
1940 let client = endpoint.client().await.unwrap();
1941
1942 let router = PushRouter::<u64, TestResponse>::from_client(client, RouterMode::RoundRobin)
1943 .await
1944 .unwrap();
1945
1946 let input: ManyIn<u64> =
1947 Context::new(RequestStream::new(Box::pin(tokio_stream::iter(vec![
1948 1u64, 2u64,
1949 ]))));
1950 let result = router.generate(input).await;
1951 assert!(
1952 result.is_err(),
1953 "bidirectional generate must bail when no instances are registered"
1954 );
1955
1956 rt.shutdown();
1957 }
1958
1959 #[tokio::test]
1960 async fn bidirectional_generate_bails_for_kv_router_mode() {
1961 let rt = Runtime::from_current().unwrap();
1962 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1963 .await
1964 .unwrap();
1965 let ns = drt.namespace("test_bidi_kv_mode".to_string()).unwrap();
1966 let component = ns.component("test_component".to_string()).unwrap();
1967 let endpoint = component.endpoint("test_endpoint".to_string());
1968 let client = endpoint.client().await.unwrap();
1969
1970 let router = PushRouter::<u64, TestResponse>::from_client(client, RouterMode::KV)
1971 .await
1972 .unwrap();
1973
1974 let input: ManyIn<u64> =
1975 Context::new(RequestStream::new(Box::pin(tokio_stream::iter(vec![1u64]))));
1976 let result = router.generate(input).await;
1977 assert!(
1978 result.is_err(),
1979 "bidirectional generate must bail for RouterMode::KV"
1980 );
1981 let err_msg = format!("{:?}", result.unwrap_err());
1982 assert!(
1983 err_msg.contains("KV") || err_msg.contains("kv"),
1984 "error should mention KV: got {err_msg}"
1985 );
1986
1987 rt.shutdown();
1988 }
1989
1990 #[tokio::test]
1991 async fn bidirectional_generate_bails_for_direct_router_mode() {
1992 let rt = Runtime::from_current().unwrap();
1993 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1994 .await
1995 .unwrap();
1996 let ns = drt.namespace("test_bidi_direct_mode".to_string()).unwrap();
1997 let component = ns.component("test_component".to_string()).unwrap();
1998 let endpoint = component.endpoint("test_endpoint".to_string());
1999 let client = endpoint.client().await.unwrap();
2000
2001 let router = PushRouter::<u64, TestResponse>::from_client(client, RouterMode::Direct)
2002 .await
2003 .unwrap();
2004
2005 let input: ManyIn<u64> =
2006 Context::new(RequestStream::new(Box::pin(tokio_stream::iter(vec![1u64]))));
2007 let result = router.generate(input).await;
2008 assert!(
2009 result.is_err(),
2010 "bidirectional generate must bail for RouterMode::Direct"
2011 );
2012 let err_msg = format!("{:?}", result.unwrap_err());
2013 assert!(
2014 err_msg.contains("Direct") || err_msg.contains("direct"),
2015 "error should mention Direct: got {err_msg}"
2016 );
2017
2018 rt.shutdown();
2019 }
2020
2021 #[tokio::test]
2022 async fn bidirectional_generate_rejects_unsupported_load_aware_modes() {
2023 let rt = Runtime::from_current().unwrap();
2024 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2025 .await
2026 .unwrap();
2027 let ns = drt.namespace("test_bidi_load_aware".to_string()).unwrap();
2028 let component = ns.component("test_component".to_string()).unwrap();
2029
2030 for mode in [
2031 RouterMode::PowerOfTwoChoices,
2032 RouterMode::LeastLoaded,
2033 RouterMode::DeviceAwareWeighted,
2034 ] {
2035 let endpoint = component.endpoint("test_endpoint".to_string());
2036 let client = endpoint.client().await.unwrap();
2037 let router = PushRouter::<u64, TestResponse>::from_client(client, mode)
2038 .await
2039 .unwrap();
2040
2041 let input: ManyIn<u64> =
2042 Context::new(RequestStream::new(Box::pin(tokio_stream::iter(vec![1u64]))));
2043 let result = router.generate(input).await;
2044 assert!(
2045 result.is_err(),
2046 "bidirectional generate must reject {mode:?} (not yet supported)"
2047 );
2048 let err_msg = format!("{:?}", result.unwrap_err());
2049 assert!(
2050 err_msg.contains("not yet supported for bidirectional dispatch"),
2051 "error should explain the mode is unsupported, not 'no instances': got {err_msg}"
2052 );
2053 }
2054
2055 rt.shutdown();
2056 }
2057
2058 #[tokio::test]
2059 async fn least_loaded_peek_returns_available_worker_select_stays_none() {
2060 let rt = Runtime::from_current().unwrap();
2061 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2062 .await
2063 .unwrap();
2064 let ns = drt
2065 .namespace("test_least_loaded_router".to_string())
2066 .unwrap();
2067 let component = ns.component("test_component".to_string()).unwrap();
2068 let endpoint = component.endpoint("test_endpoint".to_string());
2069 let client = endpoint.client().await.unwrap();
2070
2071 endpoint.register_endpoint_instance().await.unwrap();
2072 client.wait_for_instances().await.unwrap();
2073
2074 let router = PushRouter::<u64, TestResponse>::from_client(client, RouterMode::LeastLoaded)
2075 .await
2076 .unwrap();
2077
2078 assert_eq!(router.select_next_worker(), None);
2081 assert!(
2082 router.peek_next_worker().is_some(),
2083 "LeastLoaded peek must return the available worker for disagg bootstrap"
2084 );
2085
2086 rt.shutdown();
2087 }
2088
2089 #[tokio::test]
2090 async fn exact_selection_releases_occupancy_when_preparation_fails() {
2091 let rt = Runtime::from_current().unwrap();
2092 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2093 .await
2094 .unwrap();
2095 let ns = drt
2096 .namespace("test_exact_prepare_failure".to_string())
2097 .unwrap();
2098 let component = ns.component("test_component".to_string()).unwrap();
2099 let endpoint = component.endpoint("test_endpoint".to_string());
2100 let client = endpoint.client().await.unwrap();
2101 endpoint.register_endpoint_instance().await.unwrap();
2102 let worker_id = client.wait_for_instances().await.unwrap()[0].id();
2103
2104 let router = PushRouter::<u64, TestResponse>::from_client(client, RouterMode::LeastLoaded)
2105 .await
2106 .unwrap();
2107 let state = router.occupancy_state.clone().unwrap();
2108 let result = router
2109 .select_and_dispatch_exact(SingleIn::new(42), None, |_, _| {
2110 Err::<(), _>(anyhow::anyhow!("metadata preparation failed"))
2111 })
2112 .await;
2113
2114 assert!(result.is_err());
2115 assert_eq!(
2116 state.load(worker_id),
2117 0,
2118 "preparation failure must release the selected worker"
2119 );
2120 rt.shutdown();
2121 }
2122
2123 #[tokio::test]
2124 async fn exact_dispatch_revalidates_overload_after_preparation() {
2125 let rt = Runtime::from_current().unwrap();
2126 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2127 .await
2128 .unwrap();
2129 let ns = drt
2130 .namespace("test_exact_overload_revalidation".to_string())
2131 .unwrap();
2132 let component = ns.component("test_component".to_string()).unwrap();
2133 let endpoint = component.endpoint("test_endpoint".to_string());
2134 let client = endpoint.client().await.unwrap();
2135 endpoint.register_endpoint_instance().await.unwrap();
2136 let worker_id = client.wait_for_instances().await.unwrap()[0].id();
2137
2138 let router =
2139 PushRouter::<u64, TestResponse>::from_client(client.clone(), RouterMode::LeastLoaded)
2140 .await
2141 .unwrap();
2142 let state = router.occupancy_state.clone().unwrap();
2143 let result = router
2144 .select_and_dispatch_exact(SingleIn::new(42), Some(worker_id), |_, worker_id| {
2145 client.set_overloaded_instances(&[worker_id]);
2146 Ok(())
2147 })
2148 .await;
2149
2150 assert!(result.is_err());
2151 assert_eq!(
2152 state.load(worker_id),
2153 0,
2154 "validation failure must release the selected worker"
2155 );
2156 rt.shutdown();
2157 }
2158
2159 #[tokio::test]
2160 async fn selected_overloaded_worker_is_rejected_before_dispatch() {
2161 const TEST_RECONCILE_INTERVAL: std::time::Duration = std::time::Duration::from_millis(50);
2162
2163 let rt = Runtime::from_current().unwrap();
2164 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2165 .await
2166 .unwrap();
2167 let ns = drt
2168 .namespace("test_selected_overloaded_worker_rejected".to_string())
2169 .unwrap();
2170 let component = ns.component("test_component".to_string()).unwrap();
2171 let endpoint = component.endpoint("test_endpoint".to_string());
2172 let client = Client::with_reconcile_interval(endpoint.clone(), TEST_RECONCILE_INTERVAL)
2173 .await
2174 .unwrap();
2175
2176 endpoint.register_endpoint_instance().await.unwrap();
2177 let instances = client.wait_for_instances().await.unwrap();
2178 let worker_id = instances[0].id();
2179
2180 for _ in 0..10 {
2181 if client.instance_ids_avail().contains(&worker_id) {
2182 break;
2183 }
2184 tokio::time::sleep(TEST_RECONCILE_INTERVAL).await;
2185 }
2186 assert!(
2187 client.instance_ids_avail().contains(&worker_id),
2188 "worker should be routable before marking it overloaded"
2189 );
2190
2191 client.set_overloaded_instances(&[worker_id]);
2192 let router = PushRouter::<u64, TestResponse>::from_client(client, RouterMode::RoundRobin)
2193 .await
2194 .unwrap();
2195
2196 let result = router.generate(SingleIn::new(42u64)).await;
2197 assert!(result.is_err());
2198 let msg = format!("{}", result.unwrap_err());
2199 assert!(
2205 msg.contains("All workers are busy"),
2206 "expected empty-free-pool rejection, got: {msg}"
2207 );
2208
2209 rt.shutdown();
2210 }
2211
2212 #[tokio::test]
2213 async fn direct_within_rejects_overloaded_constrained_target() {
2214 const TEST_RECONCILE_INTERVAL: std::time::Duration = std::time::Duration::from_secs(3600);
2215
2216 let rt = Runtime::from_current().unwrap();
2217 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2218 .await
2219 .unwrap();
2220 let ns = drt
2221 .namespace("test_direct_within_overload_rejection".to_string())
2222 .unwrap();
2223 let component = ns.component("test_component".to_string()).unwrap();
2224 let endpoint = component.endpoint("test_endpoint".to_string());
2225 let client = Client::with_reconcile_interval(endpoint.clone(), TEST_RECONCILE_INTERVAL)
2226 .await
2227 .unwrap();
2228
2229 endpoint.register_endpoint_instance().await.unwrap();
2230 let worker_id = client.wait_for_instances().await.unwrap()[0].id();
2231 client.set_overloaded_instances(&[worker_id]);
2232
2233 let router = PushRouter::<u64, TestResponse>::from_client(client, RouterMode::RoundRobin)
2234 .await
2235 .unwrap();
2236 let allowed = HashSet::from([worker_id]);
2237 let error = router
2238 .direct_within(SingleIn::new(42), worker_id, Some(&allowed))
2239 .await
2240 .unwrap_err();
2241
2242 assert!(match_error_chain(
2243 error.as_ref(),
2244 &[ErrorType::ResourceExhausted],
2245 &[]
2246 ));
2247 assert!(
2248 error.to_string().contains("Selected worker is overloaded"),
2249 "expected overload rejection, got: {error}"
2250 );
2251
2252 rt.shutdown();
2253 }
2254
2255 #[tokio::test]
2256 async fn no_workers_is_reported_as_unavailable() {
2257 let rt = Runtime::from_current().unwrap();
2258 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2259 .await
2260 .unwrap();
2261 let ns = drt
2262 .namespace("test_no_workers_unavailable".to_string())
2263 .unwrap();
2264 let component = ns.component("test_component".to_string()).unwrap();
2265 let endpoint = component.endpoint("test_endpoint".to_string());
2266 let client = endpoint.client().await.unwrap();
2267 let router = PushRouter::<u64, TestResponse>::from_client(client, RouterMode::RoundRobin)
2268 .await
2269 .unwrap();
2270
2271 let error = router.generate(SingleIn::new(42)).await.unwrap_err();
2272 assert!(match_error_chain(
2273 error.as_ref(),
2274 &[ErrorType::Unavailable],
2275 &[]
2276 ));
2277
2278 rt.shutdown();
2279 }
2280
2281 #[tokio::test]
2282 async fn round_robin_excludes_overloaded_workers_from_candidates() {
2283 const TEST_RECONCILE_INTERVAL: std::time::Duration = std::time::Duration::from_secs(3600);
2289
2290 let rt = Runtime::from_current().unwrap();
2291 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2292 .await
2293 .unwrap();
2294 let ns = drt
2295 .namespace("test_round_robin_excludes_overloaded".to_string())
2296 .unwrap();
2297 let component = ns.component("test_component".to_string()).unwrap();
2298 let endpoint = component.endpoint("test_endpoint".to_string());
2299 let client = Client::with_reconcile_interval(endpoint.clone(), TEST_RECONCILE_INTERVAL)
2300 .await
2301 .unwrap();
2302
2303 endpoint.register_endpoint_instance().await.unwrap();
2304 let instances = client.wait_for_instances().await.unwrap();
2305 let real_id = instances[0].id();
2306 for _ in 0..50 {
2307 if client.instance_ids_avail().contains(&real_id) {
2308 break;
2309 }
2310 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
2311 }
2312
2313 client.override_instance_avail(vec![1, 2]);
2320 client.set_overloaded_instances(&[1]);
2321
2322 let router = PushRouter::<u64, TestResponse>::from_client(client, RouterMode::RoundRobin)
2323 .await
2324 .unwrap();
2325
2326 for _ in 0..6 {
2329 let selected = router
2330 .peek_next_worker()
2331 .expect("peek should succeed with a free worker");
2332 assert_eq!(
2333 selected, 2,
2334 "overloaded worker 1 must not appear in the candidate set"
2335 );
2336 }
2337
2338 rt.shutdown();
2339 }
2340
2341 #[tokio::test]
2342 async fn device_aware_cpu_only_selects_least_loaded_instance() {
2343 let state = RoutingOccupancyState::default();
2344 for _ in 0..3 {
2346 state.increment(1);
2347 }
2348 state.increment(3);
2349
2350 let instance_ids = vec![1, 2, 3];
2351 let device_type_map = HashMap::from([
2352 (1, Some(DeviceType::Cpu)),
2353 (2, Some(DeviceType::Cpu)),
2354 (3, Some(DeviceType::Cpu)),
2355 ]);
2356
2357 let candidates = device_aware_candidate_group(&state, &instance_ids, &device_type_map, 8);
2358 assert_eq!(candidates, vec![1, 2, 3]);
2359
2360 let selected = state
2361 .select_exact_min_and_increment(&candidates)
2362 .await
2363 .unwrap();
2364 assert_eq!(selected, 2);
2365 }
2366
2367 #[tokio::test]
2368 async fn device_aware_non_cpu_only_selects_least_loaded_instance() {
2369 let state = RoutingOccupancyState::default();
2370 for _ in 0..3 {
2372 state.increment(1);
2373 }
2374 state.increment(3);
2375
2376 let instance_ids = vec![1, 2, 3];
2377 let device_type_map = HashMap::from([
2378 (1, Some(DeviceType::Cuda)),
2379 (2, Some(DeviceType::Cuda)),
2380 (3, Some(DeviceType::Cuda)),
2381 ]);
2382
2383 let candidates = device_aware_candidate_group(&state, &instance_ids, &device_type_map, 8);
2384 assert_eq!(candidates, vec![1, 2, 3]);
2385
2386 let selected = state
2387 .select_exact_min_and_increment(&candidates)
2388 .await
2389 .unwrap();
2390 assert_eq!(selected, 2);
2391 }
2392
2393 #[test]
2394 fn device_aware_group_uses_ratio_budget() {
2395 let state = RoutingOccupancyState::default();
2396 for _ in 0..4 {
2398 state.increment(3);
2399 state.increment(4);
2400 }
2401 for _ in 0..3 {
2403 state.increment(1);
2404 }
2405 let instance_ids = vec![1, 2, 3, 4];
2409 let device_type_map = HashMap::from([
2410 (1, Some(DeviceType::Cpu)),
2411 (2, Some(DeviceType::Cpu)),
2412 (3, Some(DeviceType::Cuda)),
2413 (4, Some(DeviceType::Cuda)),
2414 ]);
2415
2416 let candidates = device_aware_candidate_group(&state, &instance_ids, &device_type_map, 2);
2417 assert_eq!(candidates, vec![1, 2]);
2418
2419 let selected =
2421 futures::executor::block_on(state.select_exact_min_and_increment(&candidates)).unwrap();
2422 assert_eq!(selected, 2);
2423 }
2424
2425 #[tokio::test]
2426 async fn device_aware_weighted_peek_returns_available_worker_select_stays_none() {
2427 let rt = Runtime::from_current().unwrap();
2428 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2429 .await
2430 .unwrap();
2431 let ns = drt
2432 .namespace("test_device_aware_router".to_string())
2433 .unwrap();
2434 let component = ns.component("test_component".to_string()).unwrap();
2435 let endpoint = component.endpoint("test_endpoint".to_string());
2436 let client = endpoint.client().await.unwrap();
2437
2438 endpoint.register_endpoint_instance().await.unwrap();
2439 client.wait_for_instances().await.unwrap();
2440
2441 let router =
2442 PushRouter::<u64, TestResponse>::from_client(client, RouterMode::DeviceAwareWeighted)
2443 .await
2444 .unwrap();
2445
2446 assert_eq!(router.select_next_worker(), None);
2449 assert!(
2450 router.peek_next_worker().is_some(),
2451 "DeviceAwareWeighted peek must return the available worker for disagg bootstrap"
2452 );
2453
2454 rt.shutdown();
2455 }
2456
2457 #[tokio::test]
2458 async fn device_aware_exact_selection_preserves_full_multimodal_cache_hit() {
2459 let rt = Runtime::from_current().unwrap();
2460 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2461 .await
2462 .unwrap();
2463 let ns = drt
2464 .namespace("test_device_aware_affinity_cache".to_string())
2465 .unwrap();
2466 let component = ns.component("test_component".to_string()).unwrap();
2467 let endpoint = component.endpoint("test_endpoint".to_string());
2468 let client = endpoint.client().await.unwrap();
2469 endpoint.register_endpoint_instance().await.unwrap();
2470 let cache_worker = client.wait_for_instances().await.unwrap()[0].id();
2471
2472 let router = PushRouter::<u64, TestResponse>::from_client_with_state(
2473 client,
2474 RouterMode::DeviceAwareWeighted,
2475 None,
2476 Some(Arc::new(StaticMultimodalCacheIndex {
2477 worker_id: cache_worker,
2478 })),
2479 Some(Arc::new(|_| vec!["image-key".to_string()])),
2480 )
2481 .await
2482 .unwrap();
2483
2484 let (worker_id, permit) = router.select_exact_target(&42, None).await.unwrap();
2485 assert_eq!(worker_id, cache_worker);
2486 assert!(
2487 permit.is_none(),
2488 "full cache hits bypass occupancy charging"
2489 );
2490
2491 rt.shutdown();
2492 }
2493
2494 #[tokio::test]
2498 async fn transport_resolution_falls_back_when_selected_instance_disappears() {
2499 let rt = Runtime::from_current().unwrap();
2500 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2501 .await
2502 .unwrap();
2503 let ns = drt
2504 .namespace("test_transport_fallback".to_string())
2505 .unwrap();
2506 let component = ns.component("test_component".to_string()).unwrap();
2507 let endpoint = component.endpoint("test_endpoint".to_string());
2508 let client = endpoint.client().await.unwrap();
2509
2510 endpoint.register_endpoint_instance().await.unwrap();
2512 client.wait_for_instances().await.unwrap();
2513
2514 let real_id = client.instance_ids()[0];
2515
2516 let stale_id = real_id + 1000;
2520 client.override_instance_avail(vec![stale_id, real_id]);
2521
2522 let router =
2525 PushRouter::<u64, TestResponse>::from_client(client.clone(), RouterMode::RoundRobin)
2526 .await
2527 .unwrap();
2528
2529 let request = SingleIn::new(42u64);
2536 let result = router.generate(request).await;
2537
2538 if let Err(err) = &result {
2542 let msg = format!("{err}");
2543 assert!(
2544 !msg.contains("not found"),
2545 "Transport resolution should have fallen back, but got: {msg}"
2546 );
2547 }
2548
2549 rt.shutdown();
2550 }
2551
2552 #[tokio::test]
2553 async fn prepared_dispatch_observes_worker_after_transport_fallback() {
2554 let rt = Runtime::from_current().unwrap();
2555 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2556 .await
2557 .unwrap();
2558 let endpoint = drt
2559 .namespace("test_prepared_transport_fallback".to_string())
2560 .unwrap()
2561 .component("test_component".to_string())
2562 .unwrap()
2563 .endpoint("test_endpoint".to_string());
2564 let client = endpoint.client().await.unwrap();
2565 endpoint.register_endpoint_instance().await.unwrap();
2566 let real_id = client.wait_for_instances().await.unwrap()[0].id();
2567 let stale_id = real_id.wrapping_add(1);
2568 client.override_instance_avail(vec![stale_id, real_id]);
2569 let router = PushRouter::<u64, TestResponse>::from_client(client, RouterMode::LeastLoaded)
2570 .await
2571 .unwrap();
2572 let state = router.occupancy_state.clone().unwrap();
2573 state.increment(real_id);
2574 let state_for_prepare = state.clone();
2575 let observed = Arc::new(AtomicU64::new(0));
2576 let observed_for_prepare = observed.clone();
2577
2578 let _ = tokio::time::timeout(
2579 std::time::Duration::from_millis(100),
2580 router.select_and_dispatch(SingleIn::new(42), move |_, worker_id| {
2581 assert_eq!(state_for_prepare.load(stale_id), 0);
2582 assert_eq!(state_for_prepare.load(worker_id), 2);
2583 observed_for_prepare.store(worker_id, Ordering::Relaxed);
2584 Ok(())
2585 }),
2586 )
2587 .await;
2588
2589 assert_eq!(observed.load(Ordering::Relaxed), real_id);
2590 assert_eq!(state.load(real_id), 1);
2591 state.decrement(real_id);
2592 rt.shutdown();
2593 }
2594
2595 #[tokio::test]
2598 async fn transport_resolution_errors_when_no_instances_available() {
2599 let rt = Runtime::from_current().unwrap();
2600 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2601 .await
2602 .unwrap();
2603 let ns = drt
2604 .namespace("test_transport_no_fallback".to_string())
2605 .unwrap();
2606 let component = ns.component("test_component".to_string()).unwrap();
2607 let endpoint = component.endpoint("test_endpoint".to_string());
2608 let client = endpoint.client().await.unwrap();
2609
2610 endpoint.register_endpoint_instance().await.unwrap();
2612 client.wait_for_instances().await.unwrap();
2613
2614 let router =
2615 PushRouter::<u64, TestResponse>::from_client(client.clone(), RouterMode::RoundRobin)
2616 .await
2617 .unwrap();
2618
2619 let stale_id = 99999;
2622 client.override_instance_avail(vec![stale_id]);
2623
2624 let request = SingleIn::new(42u64);
2625 let result = router.generate(request).await;
2626
2627 assert!(result.is_err());
2628 let msg = format!("{}", result.unwrap_err());
2629 assert!(
2630 msg.contains("not found") && msg.contains("no other instances available"),
2631 "Expected clear error about missing instance with no fallback, got: {msg}"
2632 );
2633
2634 rt.shutdown();
2635 }
2636
2637 #[tokio::test]
2638 async fn transport_resolution_honors_fallback_policy() {
2639 let rt = Runtime::from_current().unwrap();
2640 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2641 .await
2642 .unwrap();
2643 let ns = drt
2644 .namespace("test_exact_transport_no_fallback".to_string())
2645 .unwrap();
2646 let component = ns.component("test_component".to_string()).unwrap();
2647 let endpoint = component.endpoint("test_endpoint".to_string());
2648 let client = endpoint.client().await.unwrap();
2649 endpoint.register_endpoint_instance().await.unwrap();
2650 let instances = client.wait_for_instances().await.unwrap();
2651 let real_id = instances[0].id();
2652
2653 let router =
2654 PushRouter::<u64, TestResponse>::from_client(client.clone(), RouterMode::RoundRobin)
2655 .await
2656 .unwrap();
2657 let stale_id = real_id.wrapping_add(1);
2658 client.override_instance_avail(vec![stale_id, real_id]);
2659
2660 assert!(
2661 router
2662 .resolve_transport(stale_id, TransportFallback::Allow)
2663 .is_ok(),
2664 "normal dispatch should preserve transport fallback"
2665 );
2666 let allowed = HashSet::from([real_id]);
2667 assert!(
2668 router
2669 .resolve_transport(stale_id, TransportFallback::Within(&allowed))
2670 .is_ok(),
2671 "constrained dispatch should fall back within the allowed worker set"
2672 );
2673 let disallowed = HashSet::new();
2674 assert!(
2675 router
2676 .resolve_transport(stale_id, TransportFallback::Within(&disallowed))
2677 .is_err(),
2678 "constrained dispatch must not fall back outside the allowed worker set"
2679 );
2680 let error = router
2681 .resolve_transport(stale_id, TransportFallback::Deny)
2682 .unwrap_err();
2683 assert!(
2684 error.to_string().contains("not found"),
2685 "exact dispatch must reject the missing selected worker"
2686 );
2687
2688 rt.shutdown();
2689 }
2690
2691 #[tokio::test]
2703 async fn watcher_dedup_guard_released_on_panic() {
2704 let endpoint_id = EndpointId {
2705 namespace: "panic-test-ns".to_string(),
2706 component: "panic-test-comp".to_string(),
2707 name: "panic-test-endpoint".to_string(),
2708 };
2709
2710 let map = ENDPOINT_WATCHER_ACTIVE.get_or_init(dashmap::DashMap::new);
2712 map.insert(endpoint_id.clone(), ());
2713
2714 let endpoint_id_clone = endpoint_id.clone();
2715 let join = tokio::spawn(async move {
2716 struct GuardRelease(EndpointId);
2718 impl Drop for GuardRelease {
2719 fn drop(&mut self) {
2720 if let Some(map) = ENDPOINT_WATCHER_ACTIVE.get() {
2721 map.remove(&self.0);
2722 }
2723 }
2724 }
2725 let _release = GuardRelease(endpoint_id_clone);
2726 panic!("simulated watcher-task panic");
2727 });
2728
2729 let result = join.await;
2730 assert!(result.is_err() && result.unwrap_err().is_panic());
2731 assert!(
2732 !map.contains_key(&endpoint_id),
2733 "Drop guard must release the dedup entry even on panic"
2734 );
2735 }
2736
2737 #[tokio::test]
2741 async fn watcher_dedup_guard_released_on_normal_exit() {
2742 let endpoint_id = EndpointId {
2743 namespace: "normal-test-ns".to_string(),
2744 component: "normal-test-comp".to_string(),
2745 name: "normal-test-endpoint".to_string(),
2746 };
2747
2748 let map = ENDPOINT_WATCHER_ACTIVE.get_or_init(dashmap::DashMap::new);
2749 map.insert(endpoint_id.clone(), ());
2750
2751 let endpoint_id_clone = endpoint_id.clone();
2752 tokio::spawn(async move {
2753 struct GuardRelease(EndpointId);
2754 impl Drop for GuardRelease {
2755 fn drop(&mut self) {
2756 if let Some(map) = ENDPOINT_WATCHER_ACTIVE.get() {
2757 map.remove(&self.0);
2758 }
2759 }
2760 }
2761 let _release = GuardRelease(endpoint_id_clone);
2762 })
2764 .await
2765 .unwrap();
2766
2767 assert!(!map.contains_key(&endpoint_id));
2768 }
2769}