1use super::{AsyncEngineContextProvider, ResponseStream};
5use crate::error::{BackendError, DynamoError, ErrorType, match_error_chain};
6use crate::pipeline::network::egress::route_span::{
7 get_route_trace_context, record_route_error, record_route_span_start, wrap_route_span,
8};
9use crate::{
10 component::{Client, DeviceType, Endpoint, Instance, RoutingInstances},
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, StreamingDispatch,
17 error::{PipelineError, PipelineErrorExt},
18 },
19 protocols::{EndpointId, maybe_error::MaybeError},
20 routing_policy::{
21 CandidateView, OccupancyReservation, RouteCandidate, RouteContext, RouteDevice,
22 RoutePicker, RoutePolicy, RouteTarget, RoutingOccupancyState,
23 get_or_create_routing_occupancy_state,
24 },
25 traits::DistributedRuntimeProvider,
26};
27use async_trait::async_trait;
28use futures::Stream;
29use serde::{Deserialize, Serialize};
30use std::{
31 collections::{HashMap, HashSet},
32 marker::PhantomData,
33 pin::Pin,
34 sync::{Arc, atomic::AtomicU64},
35 task::Poll,
36 time::Instant,
37};
38use tokio_stream::StreamExt;
39use tracing::Instrument;
40
41fn is_inhibited(err: &(dyn std::error::Error + 'static)) -> bool {
43 const INHIBITED: &[ErrorType] = &[
44 ErrorType::CannotConnect,
45 ErrorType::Disconnected,
46 ErrorType::ConnectionTimeout,
47 ErrorType::ResponseTimeout,
48 ErrorType::Backend(BackendError::EngineShutdown),
49 ErrorType::Backend(BackendError::StreamIncomplete),
53 ];
54 match_error_chain(err, INHIBITED, &[])
55}
56
57fn response_inactivity_timeout() -> Option<std::time::Duration> {
61 use crate::config::environment_names::llm::DYN_HTTP_BACKEND_STREAM_TIMEOUT_SECS;
62 std::env::var(DYN_HTTP_BACKEND_STREAM_TIMEOUT_SECS)
63 .ok()
64 .and_then(|s| s.parse::<u64>().ok())
65 .filter(|&secs| secs > 0)
66 .map(std::time::Duration::from_secs)
67}
68
69struct OccupancyPermit {
73 reservation: Option<OccupancyReservation>,
74}
75
76impl OccupancyPermit {
77 fn acquire(state: Arc<RoutingOccupancyState>, instance_id: u64) -> Self {
78 Self {
79 reservation: Some(state.reserve(instance_id)),
80 }
81 }
82
83 fn from_counter(
84 state: Arc<RoutingOccupancyState>,
85 instance_id: u64,
86 counter: Arc<AtomicU64>,
87 ) -> Self {
88 Self {
89 reservation: Some(OccupancyReservation::from_counter(
90 state,
91 instance_id,
92 counter,
93 )),
94 }
95 }
96
97 fn retarget(&mut self, instance_id: u64) {
98 self.reservation
99 .as_mut()
100 .expect("occupancy permit must be armed before stream tracking")
101 .retarget(instance_id);
102 }
103
104 fn into_tracked_stream<U: Data + MaybeError>(mut self, stream: ManyOut<U>) -> ManyOut<U> {
105 let engine_ctx = stream.context();
106 ResponseStream::new(
107 Box::pin(OccupancyTrackedStream {
108 inner: stream,
109 reservation: self.reservation.take(),
110 }),
111 engine_ctx,
112 )
113 }
114}
115
116#[async_trait]
119pub trait WorkerLoadMonitor: Send + Sync {
120 async fn start_monitoring(&self) -> anyhow::Result<()>;
123}
124
125pub trait MultimodalCacheIndex: Send + Sync {
127 fn workers_with_cache_key_hits(&self, cache_keys: &[String]) -> Vec<(u64, usize)>;
128 fn remove_worker(&self, worker_id: u64);
129}
130
131pub type MultimodalCacheKeyExtractor<T> = Arc<dyn Fn(&T) -> Vec<String> + Send + Sync>;
132
133#[derive(Clone)]
134pub struct PushRouter<T, U>
135where
136 T: Data + Serialize,
137 U: Data + for<'de> Deserialize<'de>,
138{
139 pub client: Client,
142
143 router_mode: RouterMode,
150
151 picker: Option<Arc<RoutePicker>>,
153
154 round_robin_picker: Arc<RoutePicker>,
157 random_picker: Arc<RoutePicker>,
158
159 addressed: Arc<dyn StreamingDispatch<T, U>>,
163
164 fault_detection_enabled: bool,
169
170 response_timeout: Option<std::time::Duration>,
173
174 occupancy_state: Option<Arc<RoutingOccupancyState>>,
176
177 multimodal_cache_indexer: Option<Arc<dyn MultimodalCacheIndex>>,
180
181 multimodal_cache_key_extractor: Option<MultimodalCacheKeyExtractor<T>>,
183
184 _phantom: PhantomData<(T, U)>,
188}
189
190#[derive(Default, Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
191#[serde(rename_all = "snake_case")]
192pub enum RouterMode {
193 #[default]
194 RoundRobin,
195 Random,
196 PowerOfTwoChoices,
197 KV,
198 Direct,
199 LeastLoaded,
200 DeviceAwareWeighted,
202}
203
204#[derive(Clone, Copy)]
205enum TransportFallback<'a> {
206 Allow,
207 Deny,
208 Within(&'a HashSet<u64>),
209}
210
211#[derive(Clone, Copy)]
212enum OverloadCheck {
213 Required,
214 AlreadyAdmitted,
215}
216
217struct DeviceAwareCandidates {
218 candidates: Vec<RouteCandidate>,
219 context: RouteContext,
220 embedding_cache_hit: bool,
221 request_cache_keys: usize,
222}
223
224pub struct DeviceAwareSelection {
227 worker_id: u64,
228 candidate_count: usize,
229 load: u64,
230 is_cpu: bool,
231 embedding_cache_hit: bool,
232 request_cache_keys: usize,
233 reservation: Option<OccupancyReservation>,
234}
235
236impl DeviceAwareSelection {
237 pub fn worker_id(&self) -> u64 {
238 self.worker_id
239 }
240
241 pub fn candidate_count(&self) -> usize {
242 self.candidate_count
243 }
244
245 pub fn load(&self) -> u64 {
246 self.load
247 }
248
249 pub fn is_cpu(&self) -> bool {
250 self.is_cpu
251 }
252
253 pub fn embedding_cache_hit(&self) -> bool {
254 self.embedding_cache_hit
255 }
256
257 pub fn request_cache_keys(&self) -> usize {
258 self.request_cache_keys
259 }
260
261 pub fn into_reservation(self) -> Option<OccupancyReservation> {
262 self.reservation
263 }
264}
265
266impl RouterMode {
267 pub const fn telemetry_label(self) -> &'static str {
268 match self {
269 Self::RoundRobin => "round-robin",
270 Self::Random => "random",
271 Self::PowerOfTwoChoices => "power-of-two-choices",
272 Self::KV => "kv",
273 Self::Direct => "direct",
274 Self::LeastLoaded => "least-loaded",
275 Self::DeviceAwareWeighted => "device-aware-weighted",
276 }
277 }
278
279 pub fn is_kv_routing(&self) -> bool {
280 *self == RouterMode::KV
281 }
282
283 pub fn is_direct_routing(&self) -> bool {
284 *self == RouterMode::Direct
285 }
286
287 pub const fn requires_occupancy(self) -> bool {
289 matches!(
290 self,
291 Self::PowerOfTwoChoices | Self::LeastLoaded | Self::DeviceAwareWeighted
292 )
293 }
294
295 fn route_policy(self) -> Option<RoutePolicy> {
296 match self {
297 Self::RoundRobin => Some(RoutePolicy::RoundRobin),
298 Self::Random => Some(RoutePolicy::Random),
299 Self::PowerOfTwoChoices => Some(RoutePolicy::PowerOfTwoChoices),
300 Self::LeastLoaded => Some(RoutePolicy::LeastLoaded),
301 Self::DeviceAwareWeighted => Some(RoutePolicy::DeviceAwareWeighted),
302 Self::KV | Self::Direct => None,
303 }
304 }
305}
306
307fn route_pickers(
308 router_mode: RouterMode,
309) -> (Arc<RoutePicker>, Arc<RoutePicker>, Option<Arc<RoutePicker>>) {
310 let round_robin = Arc::new(RoutePicker::new(RoutePolicy::RoundRobin));
311 let random = Arc::new(RoutePicker::new(RoutePolicy::Random));
312 let configured = match router_mode {
313 RouterMode::RoundRobin => Some(round_robin.clone()),
314 RouterMode::Random => Some(random.clone()),
315 mode => mode.route_policy().map(RoutePicker::new).map(Arc::new),
316 };
317 (round_robin, random, configured)
318}
319
320#[cfg(test)]
323fn p2c_select_from(occupancy_state: &RoutingOccupancyState, instance_ids: &[u64]) -> u64 {
324 RoutePicker::new(RoutePolicy::PowerOfTwoChoices)
325 .peek(
326 CandidateView::Workers(instance_ids),
327 RouteContext::default(),
328 |id| occupancy_state.load(id),
329 )
330 .expect("p2c selection requires at least one candidate")
331 .target
332 .worker_id
333}
334
335static ENDPOINT_WATCHER_ACTIVE: std::sync::OnceLock<dashmap::DashMap<EndpointId, ()>> =
338 std::sync::OnceLock::new();
339
340#[derive(Clone, Debug, Eq, Hash, PartialEq)]
341struct RuntimeEndpointId {
342 connection_id: u64,
343 endpoint_id: EndpointId,
344}
345
346impl RuntimeEndpointId {
347 fn for_endpoint(endpoint: &Endpoint) -> Self {
348 Self {
349 connection_id: endpoint.drt().connection_id(),
350 endpoint_id: endpoint.id(),
351 }
352 }
353}
354
355static ENDPOINT_CACHE_INDEXER_WATCHER_ACTIVE: std::sync::OnceLock<
357 dashmap::DashMap<RuntimeEndpointId, ()>,
358> = std::sync::OnceLock::new();
359
360fn spawn_instance_removal_watcher<T, U>(
366 endpoint: Endpoint,
367 dispatch: Arc<dyn StreamingDispatch<T, U>>,
368 cancel_token: tokio_util::sync::CancellationToken,
369) where
370 T: Data + Serialize + 'static,
371 U: Data + for<'de> Deserialize<'de> + MaybeError + 'static,
372{
373 use crate::discovery::{
374 DiscoveryEvent, DiscoveryInstance, DiscoveryInstanceId, DiscoveryQuery,
375 };
376 use tokio_stream::StreamExt as _;
377
378 let guard = ENDPOINT_WATCHER_ACTIVE.get_or_init(dashmap::DashMap::new);
380 let endpoint_id = endpoint.id();
381 if guard.insert(endpoint_id.clone(), ()).is_some() {
382 tracing::debug!(
383 ?endpoint_id,
384 "Instance removal watcher already running for this endpoint, skipping"
385 );
386 return;
387 }
388
389 let endpoint_name = endpoint.name().to_string();
390
391 tokio::spawn(async move {
392 struct GuardRelease(EndpointId);
395 impl Drop for GuardRelease {
396 fn drop(&mut self) {
397 if let Some(map) = ENDPOINT_WATCHER_ACTIVE.get() {
398 map.remove(&self.0);
399 }
400 }
401 }
402 let _release = GuardRelease(endpoint_id);
403
404 let namespace = endpoint.component().namespace().name();
405 let component = endpoint.component().name().to_string();
406
407 const RECONNECT_BACKOFF: std::time::Duration = std::time::Duration::from_secs(5);
409 'reconnect: loop {
410 let query = DiscoveryQuery::Endpoint {
411 namespace: namespace.clone(),
412 component: component.clone(),
413 endpoint: endpoint_name.clone(),
414 };
415
416 let mut stream = match endpoint.drt().discovery().list_and_watch(query, None).await {
417 Ok(s) => s,
418 Err(e) => {
419 tracing::warn!(
420 endpoint = %endpoint_name,
421 "Failed to start instance removal watcher (will retry): {e}"
422 );
423 tokio::select! {
424 _ = tokio::time::sleep(RECONNECT_BACKOFF) => continue 'reconnect,
425 _ = cancel_token.cancelled() => break 'reconnect,
426 }
427 }
428 };
429
430 loop {
431 tokio::select! {
432 event = stream.next() => {
433 match event {
434 Some(Ok(DiscoveryEvent::Removed(id))) => {
435 if let DiscoveryInstanceId::Endpoint(eid) = &id {
436 dispatch.on_instance_removed(eid).await;
437 }
438 }
439 Some(Ok(DiscoveryEvent::Added(DiscoveryInstance::Endpoint(inst)))) => {
440 let eid: EndpointInstanceId = inst.endpoint_instance_id();
441 dispatch.on_instance_added(&eid).await;
442 }
443 Some(Ok(_)) => {}
444 Some(Err(e)) => {
445 tracing::warn!(
446 endpoint = %endpoint_name,
447 "Instance removal watcher stream error: {e}"
448 );
449 }
450 None => {
451 tracing::warn!(
452 endpoint = %endpoint_name,
453 "Instance removal watcher stream ended; reconnecting"
454 );
455 continue 'reconnect;
456 }
457 }
458 }
459 _ = cancel_token.cancelled() => {
460 break 'reconnect;
461 }
462 }
463 }
464 }
465
466 tracing::debug!(endpoint = %endpoint_name, "Instance removal watcher exiting");
467 });
468}
469
470fn spawn_multimodal_cache_cleanup_watcher(
472 endpoint: Endpoint,
473 indexer: Arc<dyn MultimodalCacheIndex>,
474 cancel_token: tokio_util::sync::CancellationToken,
475) {
476 use crate::discovery::{DiscoveryEvent, DiscoveryInstanceId, DiscoveryQuery};
477 use tokio_stream::StreamExt as _;
478
479 let guard = ENDPOINT_CACHE_INDEXER_WATCHER_ACTIVE.get_or_init(dashmap::DashMap::new);
480 let watcher_id = RuntimeEndpointId::for_endpoint(&endpoint);
481 if guard.insert(watcher_id.clone(), ()).is_some() {
482 tracing::debug!(
483 connection_id = watcher_id.connection_id,
484 ?watcher_id.endpoint_id,
485 "Multimodal cache cleanup watcher already running for this runtime endpoint, skipping"
486 );
487 return;
488 }
489
490 let endpoint_name = endpoint.name().to_string();
491 let namespace = endpoint.component().namespace().name();
492 let component = endpoint.component().name().to_string();
493
494 tokio::spawn(async move {
495 struct GuardRelease(RuntimeEndpointId);
496 impl Drop for GuardRelease {
497 fn drop(&mut self) {
498 if let Some(map) = ENDPOINT_CACHE_INDEXER_WATCHER_ACTIVE.get() {
499 map.remove(&self.0);
500 }
501 }
502 }
503 let _release = GuardRelease(watcher_id);
504
505 const RECONNECT_BACKOFF: std::time::Duration = std::time::Duration::from_secs(5);
506 'reconnect: loop {
507 let query = DiscoveryQuery::Endpoint {
508 namespace: namespace.clone(),
509 component: component.clone(),
510 endpoint: endpoint_name.clone(),
511 };
512
513 let mut stream = match endpoint.drt().discovery().list_and_watch(query, None).await {
514 Ok(stream) => stream,
515 Err(error) => {
516 tracing::warn!(
517 endpoint = %endpoint_name,
518 "Failed to start multimodal cache cleanup watcher (will retry): {error}"
519 );
520 tokio::select! {
521 _ = tokio::time::sleep(RECONNECT_BACKOFF) => continue 'reconnect,
522 _ = cancel_token.cancelled() => break 'reconnect,
523 }
524 }
525 };
526
527 loop {
528 tokio::select! {
529 event = stream.next() => {
530 match event {
531 Some(Ok(DiscoveryEvent::Removed(DiscoveryInstanceId::Endpoint(eid)))) => {
532 indexer.remove_worker(eid.instance_id);
533 }
534 Some(Ok(_)) => {}
535 Some(Err(error)) => {
536 tracing::warn!(
537 endpoint = %endpoint_name,
538 "Multimodal cache cleanup watcher stream error: {error}"
539 );
540 continue 'reconnect;
541 }
542 None => {
543 tracing::warn!(
544 endpoint = %endpoint_name,
545 "Multimodal cache cleanup watcher stream ended; reconnecting"
546 );
547 continue 'reconnect;
548 }
549 }
550 }
551 _ = cancel_token.cancelled() => break 'reconnect,
552 }
553 }
554 }
555
556 tracing::debug!(endpoint = %endpoint_name, "Multimodal cache cleanup watcher exiting");
557 });
558}
559
560async fn addressed_router(endpoint: &Endpoint) -> anyhow::Result<Arc<AddressedPushRouter>> {
561 AddressedPushRouter::from_runtime_provider(endpoint).await
562}
563
564impl<T, U> PushRouter<T, U>
565where
566 T: Data + Serialize,
567 U: Data + for<'de> Deserialize<'de> + MaybeError,
568{
569 pub fn router_mode(&self) -> RouterMode {
570 self.router_mode
571 }
572
573 pub async fn from_client(client: Client, router_mode: RouterMode) -> anyhow::Result<Self> {
575 Self::from_client_with_monitor(client, router_mode, None).await
576 }
577
578 pub async fn from_client_no_fault_detection(
584 client: Client,
585 router_mode: RouterMode,
586 ) -> anyhow::Result<Self> {
587 let addressed = addressed_router(&client.endpoint).await?;
588
589 let occupancy_state = if router_mode.requires_occupancy() {
590 Some(get_or_create_routing_occupancy_state(&client.endpoint).await)
591 } else {
592 None
593 };
594
595 let addressed: Arc<dyn StreamingDispatch<T, U>> = addressed;
597 spawn_instance_removal_watcher(
598 client.endpoint.clone(),
599 addressed.clone(),
600 client.endpoint.drt().primary_token(),
601 );
602 let (round_robin_picker, random_picker, picker) = route_pickers(router_mode);
603
604 Ok(PushRouter {
605 client,
606 addressed,
607 router_mode,
608 picker,
609 round_robin_picker,
610 random_picker,
611 fault_detection_enabled: false,
612 response_timeout: response_inactivity_timeout(),
613 occupancy_state,
614 multimodal_cache_indexer: None,
615 multimodal_cache_key_extractor: None,
616 _phantom: PhantomData,
617 })
618 }
619
620 pub async fn from_client_with_monitor(
627 client: Client,
628 router_mode: RouterMode,
629 worker_monitor: Option<Arc<dyn WorkerLoadMonitor>>,
630 ) -> anyhow::Result<Self> {
631 Self::from_client_with_state(client, router_mode, worker_monitor, None, None).await
632 }
633
634 pub async fn from_client_with_state(
636 client: Client,
637 router_mode: RouterMode,
638 worker_monitor: Option<Arc<dyn WorkerLoadMonitor>>,
639 multimodal_cache_indexer: Option<Arc<dyn MultimodalCacheIndex>>,
640 multimodal_cache_key_extractor: Option<MultimodalCacheKeyExtractor<T>>,
641 ) -> anyhow::Result<Self> {
642 let addressed = addressed_router(&client.endpoint).await?;
643
644 if let Some(monitor) = worker_monitor.as_ref() {
646 monitor.start_monitoring().await?;
647 }
648
649 let occupancy_state = if router_mode.requires_occupancy() {
650 Some(get_or_create_routing_occupancy_state(&client.endpoint).await)
651 } else {
652 None
653 };
654
655 let addressed: Arc<dyn StreamingDispatch<T, U>> = addressed;
657 spawn_instance_removal_watcher(
658 client.endpoint.clone(),
659 addressed.clone(),
660 client.endpoint.drt().primary_token(),
661 );
662
663 if let Some(indexer) = multimodal_cache_indexer.clone() {
665 spawn_multimodal_cache_cleanup_watcher(
666 client.endpoint.clone(),
667 indexer,
668 client.endpoint.drt().primary_token(),
669 );
670 }
671 let (round_robin_picker, random_picker, picker) = route_pickers(router_mode);
672
673 let router = PushRouter {
674 client,
675 addressed,
676 router_mode,
677 picker,
678 round_robin_picker,
679 random_picker,
680 fault_detection_enabled: true,
681 response_timeout: response_inactivity_timeout(),
682 occupancy_state,
683 multimodal_cache_indexer,
684 multimodal_cache_key_extractor,
685 _phantom: PhantomData,
686 };
687
688 Ok(router)
689 }
690
691 pub async fn from_client_with_dispatch(
699 client: Client,
700 router_mode: RouterMode,
701 dispatch: Arc<dyn StreamingDispatch<T, U>>,
702 ) -> anyhow::Result<Self> {
703 let occupancy_state = if router_mode.requires_occupancy() {
704 Some(get_or_create_routing_occupancy_state(&client.endpoint).await)
705 } else {
706 None
707 };
708
709 spawn_instance_removal_watcher(
710 client.endpoint.clone(),
711 dispatch.clone(),
712 client.endpoint.drt().primary_token(),
713 );
714 let (round_robin_picker, random_picker, picker) = route_pickers(router_mode);
715
716 Ok(PushRouter {
717 client,
718 addressed: dispatch,
719 router_mode,
720 picker,
721 round_robin_picker,
722 random_picker,
723 fault_detection_enabled: true,
724 response_timeout: response_inactivity_timeout(),
725 occupancy_state,
726 multimodal_cache_indexer: None,
727 multimodal_cache_key_extractor: None,
728 _phantom: PhantomData,
729 })
730 }
731
732 fn empty_free_pool_error(&self, routing_instances: &RoutingInstances) -> anyhow::Error {
735 if !routing_instances.routable_ids().is_empty() {
736 let cause = PipelineError::ServiceOverloaded(
737 "All workers are busy, please retry later".to_string(),
738 );
739 return DynamoError::builder()
740 .error_type(ErrorType::ResourceExhausted)
741 .message("All workers are busy, please retry later")
742 .cause(cause)
743 .build()
744 .into();
745 }
746 DynamoError::builder()
747 .error_type(ErrorType::Unavailable)
748 .message(format!(
749 "No workers available for endpoint {}",
750 self.client.endpoint.id()
751 ))
752 .build()
753 .into()
754 }
755
756 fn picker(&self) -> anyhow::Result<&RoutePicker> {
757 self.picker.as_deref().ok_or_else(|| {
758 anyhow::anyhow!(
759 "{:?} routing does not use a worker picker",
760 self.router_mode
761 )
762 })
763 }
764
765 fn select_untracked_worker(&self, picker: &RoutePicker) -> anyhow::Result<(u64, usize)> {
766 let routing_instances = self.client.routing_instances();
767 let candidates = routing_instances.free_ids();
768 let decision = picker
769 .select(
770 CandidateView::Workers(candidates),
771 RouteContext::default(),
772 |_| 0,
773 )
774 .ok_or_else(|| self.empty_free_pool_error(&routing_instances))?;
775 Ok((decision.target.worker_id, candidates.len()))
776 }
777
778 pub fn selectable_worker_ids(&self) -> anyhow::Result<Vec<u64>> {
783 self.with_selectable_worker_ids(<[u64]>::to_vec)
784 }
785
786 pub fn with_selectable_worker_ids<R>(
788 &self,
789 select: impl FnOnce(&[u64]) -> R,
790 ) -> anyhow::Result<R> {
791 let routing_instances = self.client.routing_instances();
792 if routing_instances.free_ids().is_empty() {
793 return Err(self.empty_free_pool_error(&routing_instances));
794 }
795 Ok(select(routing_instances.free_ids()))
796 }
797
798 pub fn routing_occupancy_state(&self) -> Option<Arc<RoutingOccupancyState>> {
800 self.occupancy_state.clone()
801 }
802
803 pub fn select_device_aware_and_reserve(
807 &self,
808 request: &T,
809 pinned_worker: Option<u64>,
810 ) -> anyhow::Result<DeviceAwareSelection> {
811 anyhow::ensure!(
812 self.router_mode == RouterMode::DeviceAwareWeighted,
813 "{:?} routing is not DeviceAwareWeighted",
814 self.router_mode
815 );
816
817 let state = self.occupancy_state()?;
818 if let Some(worker_id) = pinned_worker {
819 self.ensure_routable(worker_id)?;
820 let selection = self.device_aware_candidates(request, &[worker_id]);
821 let is_cpu = selection
822 .candidates
823 .first()
824 .is_some_and(|candidate| candidate.device == RouteDevice::Cpu);
825 let reservation = state.reserve(worker_id);
826 let load = reservation.load();
827 return Ok(DeviceAwareSelection {
828 worker_id,
829 candidate_count: 1,
830 load,
831 is_cpu,
832 embedding_cache_hit: selection.embedding_cache_hit,
833 request_cache_keys: selection.request_cache_keys,
834 reservation: Some(reservation),
835 });
836 }
837
838 let routing_instances = self.client.routing_instances();
839 let instance_ids = routing_instances.free_ids();
840 if instance_ids.is_empty() {
841 return Err(self.empty_free_pool_error(&routing_instances));
842 }
843 let selection = self.device_aware_candidates(request, instance_ids);
844 let (decision, counter) = state
845 .select_and_admit(
846 self.picker()?,
847 CandidateView::DeviceAware(&selection.candidates),
848 selection.context,
849 )
850 .ok_or_else(|| self.empty_free_pool_error(&routing_instances))?;
851 let worker_id = decision.target.worker_id;
852 let is_cpu = selection.candidates.iter().any(|candidate| {
853 candidate.target.worker_id == worker_id && candidate.device == RouteDevice::Cpu
854 });
855 let reservation = counter
856 .map(|counter| OccupancyReservation::from_counter(state.clone(), worker_id, counter));
857
858 Ok(DeviceAwareSelection {
859 worker_id,
860 candidate_count: selection.candidates.len(),
861 load: state.load(worker_id),
862 is_cpu,
863 embedding_cache_hit: selection.embedding_cache_hit,
864 request_cache_keys: selection.request_cache_keys,
865 reservation,
866 })
867 }
868
869 pub fn ensure_routable(&self, instance_id: u64) -> anyhow::Result<()> {
871 if self
872 .client
873 .routing_instances()
874 .routable_ids()
875 .contains(&instance_id)
876 {
877 return Ok(());
878 }
879 anyhow::bail!(
880 "instance_id={instance_id} not found for endpoint {}",
881 self.client.endpoint.id()
882 )
883 }
884
885 fn ensure_discovered_for_dispatch(&self, instance_id: u64) -> anyhow::Result<()> {
886 if self.client.instance_ids().contains(&instance_id) {
887 return Ok(());
888 }
889 Err(DynamoError::builder()
890 .error_type(ErrorType::CannotConnect)
891 .message(format!(
892 "instance_id={instance_id} not found for endpoint {}",
893 self.client.endpoint.id()
894 ))
895 .build()
896 .into())
897 }
898
899 pub async fn round_robin(&self, request: SingleIn<T>) -> anyhow::Result<ManyOut<U>> {
901 self.round_robin_prepared(request, |_, _| Ok(()))
902 .await
903 .map(|(_, stream)| stream)
904 }
905
906 async fn round_robin_prepared<M, F>(
907 &self,
908 request: SingleIn<T>,
909 prepare: F,
910 ) -> anyhow::Result<(M, ManyOut<U>)>
911 where
912 F: FnOnce(&mut T, u64) -> anyhow::Result<M>,
913 {
914 let (instance_id, candidate_count) =
915 self.select_untracked_worker(self.round_robin_picker.as_ref())?;
916 tracing::info!(
917 router_mode = "round-robin",
918 worker_id = instance_id,
919 candidate_count,
920 "Selected worker"
921 );
922
923 self.dispatch_selected(instance_id, request, None, prepare)
924 .await
925 }
926
927 pub async fn random(&self, request: SingleIn<T>) -> anyhow::Result<ManyOut<U>> {
929 self.random_prepared(request, |_, _| Ok(()))
930 .await
931 .map(|(_, stream)| stream)
932 }
933
934 async fn random_prepared<M, F>(
935 &self,
936 request: SingleIn<T>,
937 prepare: F,
938 ) -> anyhow::Result<(M, ManyOut<U>)>
939 where
940 F: FnOnce(&mut T, u64) -> anyhow::Result<M>,
941 {
942 let (instance_id, candidate_count) =
943 self.select_untracked_worker(self.random_picker.as_ref())?;
944 tracing::info!(
945 router_mode = "random",
946 worker_id = instance_id,
947 candidate_count,
948 "Selected worker"
949 );
950
951 self.dispatch_selected(instance_id, request, None, prepare)
952 .await
953 }
954
955 pub async fn power_of_two_choices(&self, request: SingleIn<T>) -> anyhow::Result<ManyOut<U>> {
958 self.power_of_two_choices_prepared(request, |_, _| Ok(()))
959 .await
960 .map(|(_, stream)| stream)
961 }
962
963 async fn power_of_two_choices_prepared<M, F>(
964 &self,
965 request: SingleIn<T>,
966 prepare: F,
967 ) -> anyhow::Result<(M, ManyOut<U>)>
968 where
969 F: FnOnce(&mut T, u64) -> anyhow::Result<M>,
970 {
971 let state = self.occupancy_state()?;
972 let (instance_id, counter, candidate_count) = {
973 let routing_instances = self.client.routing_instances();
974 let candidates = routing_instances.free_ids();
975 let (decision, counter) = state
976 .select_and_admit(
977 self.picker()?,
978 CandidateView::Workers(candidates),
979 RouteContext::default(),
980 )
981 .ok_or_else(|| self.empty_free_pool_error(&routing_instances))?;
982 (
983 decision.target.worker_id,
984 counter.expect("P2C selection always requests occupancy admission"),
985 candidates.len(),
986 )
987 };
988 tracing::info!(
989 router_mode = "power-of-two-choices",
990 worker_id = instance_id,
991 candidate_count,
992 load = state.load(instance_id),
993 "Selected worker"
994 );
995 let permit = OccupancyPermit::from_counter(state, instance_id, counter);
996 self.dispatch_selected(instance_id, request, Some(permit), prepare)
997 .await
998 }
999
1000 pub async fn direct(
1002 &self,
1003 request: SingleIn<T>,
1004 instance_id: u64,
1005 ) -> anyhow::Result<ManyOut<U>> {
1006 tracing::info!(
1007 router_mode = "direct",
1008 worker_id = instance_id,
1009 "Selected worker"
1010 );
1011 self.generate_with_fault_detection(instance_id, request, TransportFallback::Deny)
1012 .await
1013 }
1014
1015 pub async fn direct_within(
1022 &self,
1023 request: SingleIn<T>,
1024 instance_id: u64,
1025 allowed_fallback: Option<&HashSet<u64>>,
1026 ) -> anyhow::Result<ManyOut<U>> {
1027 self.direct_within_prepared(request, instance_id, allowed_fallback, |_, _| Ok(()))
1028 .await
1029 .map(|(_, stream)| stream)
1030 }
1031
1032 pub async fn direct_within_prepared<M, F>(
1035 &self,
1036 request: SingleIn<T>,
1037 instance_id: u64,
1038 allowed_fallback: Option<&HashSet<u64>>,
1039 prepare: F,
1040 ) -> anyhow::Result<(M, ManyOut<U>)>
1041 where
1042 F: FnOnce(&mut T, u64) -> anyhow::Result<M>,
1043 {
1044 let fallback = allowed_fallback
1045 .map(TransportFallback::Within)
1046 .unwrap_or(TransportFallback::Allow);
1047 self.direct_prepared_with_fallback(instance_id, request, fallback, prepare)
1048 .await
1049 }
1050
1051 pub async fn dispatch_preselected_prepared<M, F>(
1057 &self,
1058 request: SingleIn<T>,
1059 instance_id: u64,
1060 prepare: F,
1061 ) -> anyhow::Result<(M, ManyOut<U>)>
1062 where
1063 F: FnOnce(&mut T, u64) -> anyhow::Result<M>,
1064 {
1065 self.dispatch_preselected_with_fallback(
1066 instance_id,
1067 request,
1068 TransportFallback::Allow,
1069 prepare,
1070 )
1071 .await
1072 }
1073
1074 async fn direct_prepared_with_fallback<M, F>(
1075 &self,
1076 instance_id: u64,
1077 request: SingleIn<T>,
1078 fallback: TransportFallback<'_>,
1079 prepare: F,
1080 ) -> anyhow::Result<(M, ManyOut<U>)>
1081 where
1082 F: FnOnce(&mut T, u64) -> anyhow::Result<M>,
1083 {
1084 let selected_is_discovered = self
1088 .client
1089 .instances()
1090 .iter()
1091 .any(|instance| instance.instance_id == instance_id);
1092 if !selected_is_discovered {
1093 let fallback_is_available = self
1094 .client
1095 .routing_instances()
1096 .free_ids()
1097 .iter()
1098 .copied()
1099 .any(|candidate| {
1100 candidate != instance_id
1101 && match fallback {
1102 TransportFallback::Allow => true,
1103 TransportFallback::Deny => false,
1104 TransportFallback::Within(allowed) => allowed.contains(&candidate),
1105 }
1106 });
1107 if !fallback_is_available {
1108 self.ensure_discovered_for_dispatch(instance_id)?;
1109 }
1110 }
1111 let ((metadata, resolved_instance_id), response_stream) = self
1112 .generate_with_fault_detection_prepared(
1113 instance_id,
1114 request,
1115 fallback,
1116 |request, resolved_instance_id| {
1117 prepare(request, resolved_instance_id)
1118 .map(|metadata| (metadata, resolved_instance_id))
1119 },
1120 )
1121 .await?;
1122 tracing::info!(
1123 router_mode = self.router_mode.telemetry_label(),
1124 worker_id = resolved_instance_id,
1125 "Selected worker"
1126 );
1127 Ok((metadata, response_stream))
1128 }
1129
1130 async fn dispatch_preselected_with_fallback<M, F>(
1131 &self,
1132 instance_id: u64,
1133 request: SingleIn<T>,
1134 fallback: TransportFallback<'_>,
1135 prepare: F,
1136 ) -> anyhow::Result<(M, ManyOut<U>)>
1137 where
1138 F: FnOnce(&mut T, u64) -> anyhow::Result<M>,
1139 {
1140 self.ensure_discovered_for_dispatch(instance_id)?;
1144
1145 self.generate_with_fault_detection_prepared(instance_id, request, fallback, prepare)
1146 .await
1147 }
1148
1149 pub async fn dispatch_exact(
1154 &self,
1155 request: SingleIn<T>,
1156 instance_id: u64,
1157 ) -> anyhow::Result<ManyOut<U>> {
1158 self.generate_with_fault_detection(instance_id, request, TransportFallback::Deny)
1159 .await
1160 }
1161
1162 pub async fn dispatch_kv_admitted(
1169 &self,
1170 request: SingleIn<T>,
1171 instance_id: u64,
1172 ) -> anyhow::Result<ManyOut<U>> {
1173 if !self.router_mode.is_kv_routing() {
1174 anyhow::bail!("admitted dispatch is only valid in KV routing mode");
1175 }
1176 self.generate_with_fault_detection_inner(
1177 instance_id,
1178 request,
1179 TransportFallback::Deny,
1180 OverloadCheck::AlreadyAdmitted,
1181 )
1182 .await
1183 }
1184
1185 pub async fn select_and_dispatch_exact<M, F>(
1188 &self,
1189 mut request: SingleIn<T>,
1190 pinned_worker: Option<u64>,
1191 prepare: F,
1192 ) -> anyhow::Result<(M, ManyOut<U>)>
1193 where
1194 F: FnOnce(&mut T, u64) -> anyhow::Result<M>,
1195 {
1196 let (instance_id, permit) = self
1197 .select_exact_target(request.content(), pinned_worker)
1198 .await?;
1199 let metadata = prepare(&mut request, instance_id)?;
1200 let stream = self.dispatch_exact(request, instance_id).await?;
1201 let stream = match permit {
1202 Some(permit) => permit.into_tracked_stream(stream),
1203 None => stream,
1204 };
1205 Ok((metadata, stream))
1206 }
1207
1208 pub async fn select_and_dispatch<M, F>(
1211 &self,
1212 request: SingleIn<T>,
1213 prepare: F,
1214 ) -> anyhow::Result<(M, ManyOut<U>)>
1215 where
1216 F: FnOnce(&mut T, u64) -> anyhow::Result<M>,
1217 {
1218 match self.router_mode {
1219 RouterMode::Random => self.random_prepared(request, prepare).await,
1220 RouterMode::RoundRobin => self.round_robin_prepared(request, prepare).await,
1221 RouterMode::PowerOfTwoChoices => {
1222 self.power_of_two_choices_prepared(request, prepare).await
1223 }
1224 RouterMode::LeastLoaded => self.least_loaded_prepared(request, prepare).await,
1225 RouterMode::DeviceAwareWeighted => {
1226 self.device_aware_weighted_prepared(request, prepare).await
1227 }
1228 RouterMode::KV => anyhow::bail!("KV routing should not call select_and_dispatch"),
1229 RouterMode::Direct => anyhow::bail!(
1230 "Direct routing should use direct_within_prepared instead of select_and_dispatch"
1231 ),
1232 }
1233 }
1234
1235 async fn dispatch_selected<M, F>(
1236 &self,
1237 instance_id: u64,
1238 request: SingleIn<T>,
1239 mut permit: Option<OccupancyPermit>,
1240 prepare: F,
1241 ) -> anyhow::Result<(M, ManyOut<U>)>
1242 where
1243 F: FnOnce(&mut T, u64) -> anyhow::Result<M>,
1244 {
1245 let (metadata, stream) = self
1246 .generate_with_fault_detection_prepared(
1247 instance_id,
1248 request,
1249 TransportFallback::Allow,
1250 |request, resolved_instance_id| {
1251 if let Some(permit) = permit.as_mut() {
1252 permit.retarget(resolved_instance_id);
1253 }
1254 prepare(request, resolved_instance_id)
1255 },
1256 )
1257 .await?;
1258 let stream = match permit {
1259 Some(permit) => permit.into_tracked_stream(stream),
1260 None => stream,
1261 };
1262 Ok((metadata, stream))
1263 }
1264
1265 pub async fn device_aware_weighted(&self, request: SingleIn<T>) -> anyhow::Result<ManyOut<U>> {
1274 self.device_aware_weighted_prepared(request, |_, _| Ok(()))
1275 .await
1276 .map(|(_, stream)| stream)
1277 }
1278
1279 async fn device_aware_weighted_prepared<M, F>(
1280 &self,
1281 request: SingleIn<T>,
1282 prepare: F,
1283 ) -> anyhow::Result<(M, ManyOut<U>)>
1284 where
1285 F: FnOnce(&mut T, u64) -> anyhow::Result<M>,
1286 {
1287 let state = self.occupancy_state()?;
1288 let routing_instances = self.client.routing_instances();
1289 let instance_ids = routing_instances.free_ids();
1290
1291 if instance_ids.is_empty() {
1292 return Err(self.empty_free_pool_error(&routing_instances));
1293 }
1294
1295 let endpoint_id = self.client.endpoint.id();
1297
1298 let selection = self.device_aware_candidates(request.content(), instance_ids);
1299
1300 let (decision, counter) = state
1303 .select_and_admit(
1304 self.picker()?,
1305 CandidateView::DeviceAware(&selection.candidates),
1306 selection.context,
1307 )
1308 .ok_or_else(|| self.empty_free_pool_error(&routing_instances))?;
1309 let instance_id = decision.target.worker_id;
1310 let permit = counter
1311 .map(|counter| OccupancyPermit::from_counter(state.clone(), instance_id, counter));
1312 let is_cpu = selection.candidates.iter().any(|candidate| {
1313 candidate.target.worker_id == instance_id && candidate.device == RouteDevice::Cpu
1314 });
1315 tracing::info!(
1316 router_mode = "device-aware-weighted",
1317 worker_id = instance_id,
1318 candidate_count = selection.candidates.len(),
1319 load = state.load(instance_id),
1320 endpoint = %endpoint_id,
1321 is_cpu,
1322 embedding_cache_hit = selection.embedding_cache_hit,
1323 request_cache_keys = selection.request_cache_keys,
1324 "Selected worker"
1325 );
1326
1327 self.dispatch_selected(instance_id, request, permit, prepare)
1328 .await
1329 }
1330
1331 fn device_aware_candidates(&self, request: &T, instance_ids: &[u64]) -> DeviceAwareCandidates {
1332 let device_type_map = self
1333 .client
1334 .instances()
1335 .iter()
1336 .map(|instance| {
1337 let device = if matches!(instance.device_type, Some(DeviceType::Cpu)) {
1338 RouteDevice::Cpu
1339 } else {
1340 RouteDevice::Accelerator
1341 };
1342 (instance.instance_id, device)
1343 })
1344 .collect::<HashMap<_, _>>();
1345 let cuda_to_cpu_ratio = std::env::var("DYN_ENCODER_CUDA_TO_CPU_RATIO")
1346 .ok()
1347 .and_then(|value| value.parse::<usize>().ok())
1348 .filter(|value| *value >= 1)
1349 .unwrap_or(8);
1350
1351 let (request_cache_keys, cache_matched_candidates) =
1352 if let (Some(indexer), Some(extractor)) = (
1353 self.multimodal_cache_indexer.as_ref(),
1354 self.multimodal_cache_key_extractor.as_ref(),
1355 ) {
1356 let request_cache_keys = extractor(request);
1357 let matched = if request_cache_keys.is_empty() {
1358 Vec::new()
1359 } else {
1360 let mut matched = indexer.workers_with_cache_key_hits(&request_cache_keys);
1361 matched.retain(|(id, _)| instance_ids.contains(id));
1362 matched
1363 };
1364 (request_cache_keys, matched)
1365 } else {
1366 (Vec::new(), Vec::new())
1367 };
1368
1369 let embedding_cache_hit = !cache_matched_candidates.is_empty();
1370 let cache_hits = cache_matched_candidates
1371 .into_iter()
1372 .collect::<HashMap<_, _>>();
1373 let request_cache_key_count = request_cache_keys
1374 .iter()
1375 .collect::<std::collections::HashSet<_>>()
1376 .len();
1377 let candidates = instance_ids
1378 .iter()
1379 .map(|worker_id| RouteCandidate {
1380 target: RouteTarget::worker(*worker_id),
1381 device: device_type_map.get(worker_id).copied().unwrap_or_default(),
1382 cache_hits: cache_hits.get(worker_id).copied().unwrap_or_default(),
1383 })
1384 .collect::<Vec<_>>();
1385
1386 DeviceAwareCandidates {
1387 candidates,
1388 context: RouteContext {
1389 required_cache_hits: request_cache_key_count,
1390 non_cpu_to_cpu_ratio: cuda_to_cpu_ratio,
1391 },
1392 embedding_cache_hit,
1393 request_cache_keys: request_cache_keys.len(),
1394 }
1395 }
1396
1397 pub async fn least_loaded(&self, request: SingleIn<T>) -> anyhow::Result<ManyOut<U>> {
1399 self.least_loaded_prepared(request, |_, _| Ok(()))
1400 .await
1401 .map(|(_, stream)| stream)
1402 }
1403
1404 async fn least_loaded_prepared<M, F>(
1405 &self,
1406 request: SingleIn<T>,
1407 prepare: F,
1408 ) -> anyhow::Result<(M, ManyOut<U>)>
1409 where
1410 F: FnOnce(&mut T, u64) -> anyhow::Result<M>,
1411 {
1412 let state = self.occupancy_state()?;
1413 let routing_instances = self.client.routing_instances();
1414 let instance_ids = routing_instances.free_ids();
1415 let (decision, counter) = state
1416 .select_and_admit(
1417 self.picker()?,
1418 CandidateView::Workers(instance_ids),
1419 RouteContext::default(),
1420 )
1421 .ok_or_else(|| self.empty_free_pool_error(&routing_instances))?;
1422 let instance_id = decision.target.worker_id;
1423 let permit = OccupancyPermit::from_counter(
1424 state.clone(),
1425 instance_id,
1426 counter.expect("least-loaded selection always requests occupancy admission"),
1427 );
1428 tracing::info!(
1429 router_mode = "least-loaded",
1430 worker_id = instance_id,
1431 candidate_count = instance_ids.len(),
1432 load = state.load(instance_id),
1433 "Selected worker"
1434 );
1435
1436 self.dispatch_selected(instance_id, request, Some(permit), prepare)
1437 .await
1438 }
1439
1440 pub fn select_next_worker(&self) -> Option<u64> {
1444 let routing_instances = self.client.routing_instances();
1445 match self.router_mode {
1446 RouterMode::RoundRobin | RouterMode::Random => self
1447 .picker
1448 .as_deref()?
1449 .select(
1450 CandidateView::Workers(routing_instances.free_ids()),
1451 RouteContext::default(),
1452 |_| 0,
1453 )
1454 .map(|decision| decision.target.worker_id),
1455 RouterMode::PowerOfTwoChoices
1456 | RouterMode::Direct
1457 | RouterMode::LeastLoaded
1458 | RouterMode::DeviceAwareWeighted => None,
1459 RouterMode::KV => {
1460 panic!(
1461 "select_next_worker should not be called for {:?} routing mode",
1462 self.router_mode
1463 )
1464 }
1465 }
1466 }
1467
1468 pub fn peek_next_worker(&self) -> Option<u64> {
1474 let routing_instances = self.client.routing_instances();
1477 let instance_ids = routing_instances.free_ids();
1478 if instance_ids.is_empty() {
1479 return None;
1480 }
1481
1482 match self.router_mode {
1483 RouterMode::RoundRobin | RouterMode::Random => self
1484 .picker
1485 .as_deref()?
1486 .peek(
1487 CandidateView::Workers(instance_ids),
1488 RouteContext::default(),
1489 |_| 0,
1490 )
1491 .map(|decision| decision.target.worker_id),
1492 RouterMode::LeastLoaded | RouterMode::PowerOfTwoChoices => self
1493 .occupancy_state
1494 .as_deref()?
1495 .peek(
1496 self.picker.as_deref()?,
1497 CandidateView::Workers(instance_ids),
1498 RouteContext::default(),
1499 )
1500 .map(|decision| decision.target.worker_id),
1501 RouterMode::DeviceAwareWeighted => {
1502 let state = self.occupancy_state.as_deref()?;
1503 let device_type_map: HashMap<u64, Option<DeviceType>> = self
1504 .client
1505 .instances()
1506 .iter()
1507 .map(|instance| (instance.instance_id, instance.device_type.clone()))
1508 .collect();
1509 let cuda_to_cpu_ratio = std::env::var("DYN_ENCODER_CUDA_TO_CPU_RATIO")
1510 .ok()
1511 .and_then(|value| value.parse::<usize>().ok())
1512 .filter(|value| *value >= 1)
1513 .unwrap_or(8);
1514 let candidates = instance_ids
1515 .iter()
1516 .map(|worker_id| RouteCandidate {
1517 target: RouteTarget::worker(*worker_id),
1518 device: if matches!(
1519 device_type_map.get(worker_id),
1520 Some(Some(DeviceType::Cpu))
1521 ) {
1522 RouteDevice::Cpu
1523 } else {
1524 RouteDevice::Accelerator
1525 },
1526 cache_hits: 0,
1527 })
1528 .collect::<Vec<_>>();
1529 state
1530 .peek(
1531 self.picker.as_deref()?,
1532 CandidateView::DeviceAware(&candidates),
1533 RouteContext {
1534 required_cache_hits: 0,
1535 non_cpu_to_cpu_ratio: cuda_to_cpu_ratio,
1536 },
1537 )
1538 .map(|decision| decision.target.worker_id)
1539 }
1540 RouterMode::Direct => None,
1541 RouterMode::KV => {
1542 panic!(
1543 "peek_next_worker should not be called for {:?} routing mode",
1544 self.router_mode
1545 )
1546 }
1547 }
1548 }
1549
1550 #[cfg(any(test, feature = "testing"))]
1551 #[doc(hidden)]
1552 pub fn occupancy_for_test(&self, worker_id: u64) -> u64 {
1553 self.occupancy_state
1554 .as_deref()
1555 .map(|state| state.load(worker_id))
1556 .unwrap_or(0)
1557 }
1558
1559 async fn select_exact_target(
1560 &self,
1561 request: &T,
1562 pinned_worker: Option<u64>,
1563 ) -> anyhow::Result<(u64, Option<OccupancyPermit>)> {
1564 if let Some(instance_id) = pinned_worker {
1565 let routing_instances = self.client.routing_instances();
1566 if !routing_instances.routable_ids().contains(&instance_id) {
1567 return Err(anyhow::anyhow!(
1568 "instance_id={instance_id} not found for endpoint {}",
1569 self.client.endpoint.id()
1570 ));
1571 }
1572 let permit = match self.router_mode {
1573 RouterMode::LeastLoaded
1574 | RouterMode::PowerOfTwoChoices
1575 | RouterMode::DeviceAwareWeighted => {
1576 let state = self.occupancy_state()?;
1577 Some(OccupancyPermit::acquire(state, instance_id))
1578 }
1579 RouterMode::RoundRobin
1580 | RouterMode::Random
1581 | RouterMode::Direct
1582 | RouterMode::KV => None,
1583 };
1584 return Ok((instance_id, permit));
1585 }
1586
1587 match self.router_mode {
1588 RouterMode::LeastLoaded
1589 | RouterMode::PowerOfTwoChoices
1590 | RouterMode::DeviceAwareWeighted => {
1591 let state = self.occupancy_state()?;
1592 let routing_instances = self.client.routing_instances();
1593 let instance_ids = routing_instances.free_ids();
1594 if instance_ids.is_empty() {
1595 return Err(self.empty_free_pool_error(&routing_instances));
1596 }
1597
1598 let (decision, counter) = match self.router_mode {
1599 RouterMode::LeastLoaded | RouterMode::PowerOfTwoChoices => state
1600 .select_and_admit(
1601 self.picker()?,
1602 CandidateView::Workers(instance_ids),
1603 RouteContext::default(),
1604 )
1605 .ok_or_else(|| self.empty_free_pool_error(&routing_instances))?,
1606 RouterMode::DeviceAwareWeighted => {
1607 let selection = self.device_aware_candidates(request, instance_ids);
1608 state
1609 .select_and_admit(
1610 self.picker()?,
1611 CandidateView::DeviceAware(&selection.candidates),
1612 selection.context,
1613 )
1614 .ok_or_else(|| self.empty_free_pool_error(&routing_instances))?
1615 }
1616 _ => unreachable!(),
1617 };
1618 let instance_id = decision.target.worker_id;
1619 let permit = counter
1620 .map(|counter| OccupancyPermit::from_counter(state, instance_id, counter));
1621 Ok((instance_id, permit))
1622 }
1623 RouterMode::RoundRobin => self
1624 .select_untracked_worker(self.round_robin_picker.as_ref())
1625 .map(|(instance_id, _)| (instance_id, None)),
1626 RouterMode::Random => self
1627 .select_untracked_worker(self.random_picker.as_ref())
1628 .map(|(instance_id, _)| (instance_id, None)),
1629 RouterMode::Direct => Err(anyhow::anyhow!(
1630 "Worker ID required for exact dispatch in Direct routing mode"
1631 )),
1632 RouterMode::KV => Err(anyhow::anyhow!(
1633 "select_and_dispatch_exact cannot select workers in KV routing mode"
1634 )),
1635 }
1636 }
1637
1638 fn occupancy_state(&self) -> anyhow::Result<Arc<RoutingOccupancyState>> {
1639 self.occupancy_state.clone().ok_or_else(|| {
1640 anyhow::anyhow!(
1641 "routing occupancy state not initialized for endpoint {}",
1642 self.client.endpoint.id()
1643 )
1644 })
1645 }
1646
1647 async fn generate_with_fault_detection(
1658 &self,
1659 instance_id: u64,
1660 request: SingleIn<T>,
1661 fallback: TransportFallback<'_>,
1662 ) -> anyhow::Result<ManyOut<U>> {
1663 self.generate_with_fault_detection_inner(
1664 instance_id,
1665 request,
1666 fallback,
1667 OverloadCheck::Required,
1668 )
1669 .await
1670 }
1671
1672 async fn generate_with_fault_detection_inner(
1673 &self,
1674 instance_id: u64,
1675 request: SingleIn<T>,
1676 fallback: TransportFallback<'_>,
1677 overload_check: OverloadCheck,
1678 ) -> anyhow::Result<ManyOut<U>> {
1679 self.generate_with_fault_detection_prepared_inner(
1680 instance_id,
1681 request,
1682 fallback,
1683 overload_check,
1684 |_, _| Ok(()),
1685 )
1686 .await
1687 .map(|(_, stream)| stream)
1688 }
1689
1690 async fn generate_with_fault_detection_prepared<M, F>(
1691 &self,
1692 instance_id: u64,
1693 request: SingleIn<T>,
1694 fallback: TransportFallback<'_>,
1695 prepare: F,
1696 ) -> anyhow::Result<(M, ManyOut<U>)>
1697 where
1698 F: FnOnce(&mut T, u64) -> anyhow::Result<M>,
1699 {
1700 self.generate_with_fault_detection_prepared_inner(
1701 instance_id,
1702 request,
1703 fallback,
1704 OverloadCheck::Required,
1705 prepare,
1706 )
1707 .await
1708 }
1709
1710 async fn generate_with_fault_detection_prepared_inner<M, F>(
1711 &self,
1712 instance_id: u64,
1713 mut request: SingleIn<T>,
1714 fallback: TransportFallback<'_>,
1715 overload_check: OverloadCheck,
1716 prepare: F,
1717 ) -> anyhow::Result<(M, ManyOut<U>)>
1718 where
1719 F: FnOnce(&mut T, u64) -> anyhow::Result<M>,
1720 {
1721 let route_start = Instant::now();
1722 let request_id = request.id().to_string();
1723 let route_trace_context = get_route_trace_context(&request);
1724 let route_span = if matches!(self.router_mode, RouterMode::KV) {
1725 tracing::Span::none()
1726 } else {
1727 tracing::info_span!(
1728 target: "request_span",
1729 "router.route_request",
1730 otel.kind = "client",
1731 request_id = %request_id,
1732 worker_id = tracing::field::Empty,
1733 router_mode = ?self.router_mode,
1734 "request.attempt" = tracing::field::Empty,
1735 "request.outcome" = tracing::field::Empty,
1736 "migration.is_retry" = tracing::field::Empty,
1737 "migration.reason" = tracing::field::Empty,
1738 "migration.from_worker_id" = tracing::field::Empty,
1739 "migration.tokens_completed" = tracing::field::Empty,
1740 "cancellation.signal" = tracing::field::Empty,
1741 "error.type" = tracing::field::Empty,
1742 otel.status_code = tracing::field::Empty,
1743 otel.status_description = tracing::field::Empty,
1744 )
1745 };
1746 let (instance_id, address, transport_kind, instance) = match self
1747 .resolve_transport(instance_id, fallback)
1748 {
1749 Ok(resolved) => resolved,
1750 Err(error) => {
1751 record_route_span_start(&route_span, route_trace_context.as_deref(), instance_id);
1752 record_route_error(&route_span, error.as_ref());
1753 return Err(error);
1754 }
1755 };
1756 record_route_span_start(&route_span, route_trace_context.as_deref(), instance_id);
1757 if matches!(overload_check, OverloadCheck::Required)
1758 && let Err(error) = self.check_workers_available(instance_id, &request_id)
1759 {
1760 record_route_error(&route_span, error.as_ref());
1761 return Err(error);
1762 }
1763
1764 let metadata = match prepare(&mut request, instance_id) {
1765 Ok(metadata) => metadata,
1766 Err(error) => {
1767 record_route_error(&route_span, error.as_ref());
1768 return Err(error);
1769 }
1770 };
1771 let request = request.map(|req| AddressedRequest::with_instance(req, address, instance));
1772
1773 STAGE_DURATION_SECONDS
1774 .with_label_values(&[STAGE_ROUTE])
1775 .observe(route_start.elapsed().as_secs_f64());
1776
1777 let _nvtx_transport = dynamo_nvtx_range!(transport_kind);
1778 let stream = self
1779 .addressed
1780 .generate(request)
1781 .instrument(route_span.clone())
1782 .await;
1783 let stream = self.wrap_with_fault_detection(stream, instance_id, route_span)?;
1784 Ok((metadata, stream))
1785 }
1786
1787 fn check_workers_available(&self, instance_id: u64, request_id: &str) -> anyhow::Result<()> {
1792 if !self.fault_detection_enabled {
1793 return Ok(());
1794 }
1795 let routing_instances = self.client.routing_instances();
1796 let selected_worker_overloaded = routing_instances.is_overloaded(instance_id);
1797 let counts = routing_instances.counts();
1798 if tracing::enabled!(tracing::Level::DEBUG) {
1799 tracing::debug!(
1800 request_id,
1801 instance_id,
1802 router_mode = ?self.router_mode,
1803 free_workers = counts.free,
1804 overloaded_workers = counts.overloaded,
1805 total_workers = counts.discovered,
1806 selected_worker_overloaded,
1807 "checked worker overload state"
1808 );
1809 }
1810 if !selected_worker_overloaded {
1811 return Ok(());
1812 }
1813 tracing::warn!(
1814 instance_id,
1815 overloaded_workers = counts.overloaded,
1816 total_workers = counts.discovered,
1817 "Rejecting request: selected worker is overloaded"
1818 );
1819 let cause = PipelineError::ServiceOverloaded(
1820 "Selected worker is overloaded, please retry later".into(),
1821 );
1822 Err(DynamoError::builder()
1823 .error_type(ErrorType::WorkerOverloaded)
1824 .message("Selected worker is overloaded, please retry later")
1825 .cause(cause)
1826 .build()
1827 .into())
1828 }
1829
1830 fn resolve_transport(
1836 &self,
1837 instance_id: u64,
1838 fallback: TransportFallback<'_>,
1839 ) -> anyhow::Result<(u64, String, &'static str, Instance)> {
1840 use crate::component::TransportType;
1841
1842 let lookup = |id: u64| {
1843 self.client
1844 .instances()
1845 .iter()
1846 .find(|i| i.instance_id == id)
1847 .map(|instance| {
1848 let (addr, kind) = match &instance.transport {
1849 TransportType::Tcp(tcp_endpoint) => {
1850 (tcp_endpoint.clone(), "transport.tcp.request")
1851 }
1852 TransportType::Nats(subject) => (subject.clone(), "transport.nats.request"),
1853 };
1854 (addr, kind, instance.clone())
1855 })
1856 };
1857
1858 if let Some((addr, kind, inst)) = lookup(instance_id) {
1859 return Ok((instance_id, addr, kind, inst));
1860 }
1861 let allowed_fallback = match fallback {
1862 TransportFallback::Allow => None,
1863 TransportFallback::Deny => {
1864 return Err(DynamoError::builder()
1865 .error_type(ErrorType::CannotConnect)
1866 .message(format!(
1867 "instance_id={instance_id} not found for endpoint {}",
1868 self.client.endpoint.id()
1869 ))
1870 .build()
1871 .into());
1872 }
1873 TransportFallback::Within(allowed) => Some(allowed),
1874 };
1875
1876 let routing_instances = self.client.routing_instances();
1877 let fallback_id = routing_instances.free_ids().iter().copied().find(|&id| {
1878 id != instance_id && allowed_fallback.is_none_or(|allowed| allowed.contains(&id))
1879 });
1880 match fallback_id {
1881 Some(id) => {
1882 tracing::warn!(
1883 original_instance = instance_id,
1884 fallback_instance = id,
1885 "Instance disappeared during routing, reselecting"
1886 );
1887 let (addr, kind, inst) = lookup(id).ok_or_else(|| {
1888 DynamoError::builder()
1889 .error_type(ErrorType::CannotConnect)
1890 .message(format!(
1891 "Fallback instance {} also not found for endpoint {}",
1892 id,
1893 self.client.endpoint.id()
1894 ))
1895 .build()
1896 })?;
1897 Ok((id, addr, kind, inst))
1898 }
1899 None => Err(DynamoError::builder()
1912 .error_type(ErrorType::Unavailable)
1913 .message(format!(
1914 "Instance {} not found and no other instances available for endpoint {}",
1915 instance_id,
1916 self.client.endpoint.id()
1917 ))
1918 .build()
1919 .into()),
1920 }
1921 }
1922
1923 fn wrap_with_fault_detection(
1928 &self,
1929 stream: anyhow::Result<ManyOut<U>>,
1930 instance_id: u64,
1931 route_span: tracing::Span,
1932 ) -> anyhow::Result<ManyOut<U>> {
1933 let stream = match stream {
1934 Ok(stream) => stream,
1935 Err(err) => {
1936 record_route_error(&route_span, err.as_ref());
1937 if self.fault_detection_enabled {
1938 if is_inhibited(err.as_ref()) {
1939 tracing::debug!(
1940 "Reporting instance {instance_id} down due to error: {err}"
1941 );
1942 self.client.report_instance_down(instance_id);
1943 } else if match_error_chain(err.as_ref(), &[ErrorType::WorkerOverloaded], &[]) {
1944 tracing::debug!(
1950 "Marking instance {instance_id} overloaded due to backpressure: {err}"
1951 );
1952 self.client.mark_overloaded_immediate(instance_id);
1953 }
1954 }
1955 return Err(err);
1956 }
1957 };
1958
1959 if !self.fault_detection_enabled {
1960 return Ok(wrap_route_span(stream, route_span));
1961 }
1962
1963 let engine_ctx = stream.context();
1964 let client = self.client.clone();
1965 let client_for_timeout = self.client.clone();
1966 let stream = stream.map(move |res| {
1967 if let Some(err) = res.err()
1968 && is_inhibited(&err)
1969 {
1970 tracing::debug!(
1971 "Reporting instance {instance_id} down due to migratable error: {err}"
1972 );
1973 client.report_instance_down(instance_id);
1974 }
1975 res
1976 });
1977
1978 let stream: Pin<Box<dyn Stream<Item = U> + Send>> =
1979 if let Some(timeout) = self.response_timeout {
1980 Box::pin(async_stream::stream! {
1981 let mut inner = Box::pin(stream);
1982 loop {
1983 tokio::select! {
1984 biased;
1985 item = inner.next() => {
1986 match item {
1987 Some(item) => yield item,
1988 None => break,
1989 }
1990 }
1991 _ = tokio::time::sleep(timeout) => {
1992 tracing::warn!(
1993 instance_id,
1994 timeout_secs = timeout.as_secs(),
1995 "backend response inactivity timeout — quarantining worker"
1996 );
1997 client_for_timeout.report_instance_down(instance_id);
1998 yield U::from_err(
1999 crate::error::DynamoError::builder()
2000 .error_type(crate::error::ErrorType::ResponseTimeout)
2001 .message("backend response inactivity timeout")
2002 .build()
2003 );
2004 break;
2005 }
2006 }
2007 }
2008 })
2009 } else {
2010 Box::pin(stream)
2011 };
2012
2013 Ok(wrap_route_span(
2014 ResponseStream::new(stream, engine_ctx),
2015 route_span,
2016 ))
2017 }
2018}
2019
2020#[async_trait]
2021impl<T, U> AsyncEngine<SingleIn<T>, ManyOut<U>, Error> for PushRouter<T, U>
2022where
2023 T: Data + Serialize,
2024 U: Data + for<'de> Deserialize<'de> + MaybeError,
2025{
2026 async fn generate(&self, request: SingleIn<T>) -> Result<ManyOut<U>, Error> {
2027 match self.router_mode {
2028 RouterMode::Random => self.random(request).await,
2029 RouterMode::RoundRobin => self.round_robin(request).await,
2030 RouterMode::PowerOfTwoChoices => self.power_of_two_choices(request).await,
2031 RouterMode::KV => {
2032 anyhow::bail!("KV routing should not call generate on PushRouter");
2033 }
2034 RouterMode::Direct => {
2035 anyhow::bail!(
2036 "Direct routing should not call generate on PushRouter directly; use RoutingHost"
2037 );
2038 }
2039 RouterMode::LeastLoaded => self.least_loaded(request).await,
2040 RouterMode::DeviceAwareWeighted => self.device_aware_weighted(request).await,
2041 }
2042 }
2043}
2044
2045impl<T, U> PushRouter<T, U>
2046where
2047 T: Data + Serialize,
2048 U: Data + for<'de> Deserialize<'de> + MaybeError,
2049{
2050 async fn bidirectional_dispatch(
2052 &self,
2053 instance_id: u64,
2054 input: ManyIn<T>,
2055 ) -> anyhow::Result<ManyOut<U>> {
2056 let route_start = Instant::now();
2057 let request_id = input.context().id().to_string();
2058 let route_trace_context = get_route_trace_context(&input);
2059 let route_span = tracing::info_span!(
2060 target: "request_span",
2061 "router.route_request_bidirectional",
2062 otel.kind = "client",
2063 request_id = %request_id,
2064 worker_id = tracing::field::Empty,
2065 router_mode = ?self.router_mode,
2066 "request.attempt" = tracing::field::Empty,
2067 "request.outcome" = tracing::field::Empty,
2068 "migration.is_retry" = tracing::field::Empty,
2069 "migration.reason" = tracing::field::Empty,
2070 "migration.from_worker_id" = tracing::field::Empty,
2071 "migration.tokens_completed" = tracing::field::Empty,
2072 "cancellation.signal" = tracing::field::Empty,
2073 "error.type" = tracing::field::Empty,
2074 otel.status_code = tracing::field::Empty,
2075 otel.status_description = tracing::field::Empty,
2076 );
2077
2078 let (instance_id, address, transport_kind, instance) = match self
2079 .resolve_transport(instance_id, TransportFallback::Allow)
2080 {
2081 Ok(resolved) => resolved,
2082 Err(error) => {
2083 record_route_span_start(&route_span, route_trace_context.as_deref(), instance_id);
2084 record_route_error(&route_span, error.as_ref());
2085 return Err(error);
2086 }
2087 };
2088 record_route_span_start(&route_span, route_trace_context.as_deref(), instance_id);
2089 if let Err(error) = self.check_workers_available(instance_id, &request_id) {
2090 record_route_error(&route_span, error.as_ref());
2091 return Err(error);
2092 }
2093
2094 STAGE_DURATION_SECONDS
2095 .with_label_values(&[STAGE_ROUTE])
2096 .observe(route_start.elapsed().as_secs_f64());
2097
2098 let _nvtx_transport = dynamo_nvtx_range!(transport_kind);
2099 let stream: anyhow::Result<ManyOut<U>> = self
2100 .addressed
2101 .generate_bidirectional(instance, address, input)
2102 .instrument(route_span.clone())
2103 .await;
2104 self.wrap_with_fault_detection(stream, instance_id, route_span)
2105 }
2106}
2107
2108#[async_trait]
2122impl<T, U> AsyncEngine<ManyIn<T>, ManyOut<U>, Error> for PushRouter<T, U>
2123where
2124 T: Data + Serialize,
2125 U: Data + for<'de> Deserialize<'de> + MaybeError,
2126{
2127 async fn generate(&self, input: ManyIn<T>) -> Result<ManyOut<U>, Error> {
2128 match self.router_mode {
2129 RouterMode::KV => {
2130 anyhow::bail!("KV routing should not call generate on PushRouter");
2131 }
2132 RouterMode::Direct => {
2133 anyhow::bail!(
2134 "Direct routing should not call generate on PushRouter directly; use RoutingHost"
2135 );
2136 }
2137 RouterMode::PowerOfTwoChoices
2144 | RouterMode::LeastLoaded
2145 | RouterMode::DeviceAwareWeighted => {
2146 anyhow::bail!(
2147 "{:?} routing is not yet supported for bidirectional dispatch",
2148 self.router_mode
2149 );
2150 }
2151 RouterMode::RoundRobin | RouterMode::Random => {}
2152 }
2153
2154 let instance_id = self
2155 .select_next_worker()
2156 .ok_or_else(|| anyhow::anyhow!("no instances available for bidirectional routing"))?;
2157
2158 self.bidirectional_dispatch(instance_id, input).await
2159 }
2160}
2161
2162struct OccupancyTrackedStream<U: Data + MaybeError> {
2163 inner: ManyOut<U>,
2164 reservation: Option<OccupancyReservation>,
2165}
2166
2167impl<U: Data + MaybeError> OccupancyTrackedStream<U> {
2168 fn release(&mut self) {
2169 drop(self.reservation.take());
2170 }
2171}
2172
2173impl<U: Data + MaybeError> Drop for OccupancyTrackedStream<U> {
2174 fn drop(&mut self) {
2175 self.release();
2176 }
2177}
2178
2179impl<U: Data + MaybeError> std::fmt::Debug for OccupancyTrackedStream<U> {
2180 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2181 f.debug_struct("OccupancyTrackedStream")
2182 .field(
2183 "instance_id",
2184 &self
2185 .reservation
2186 .as_ref()
2187 .map(OccupancyReservation::worker_id),
2188 )
2189 .finish()
2190 }
2191}
2192
2193impl<U: Data + MaybeError> Stream for OccupancyTrackedStream<U> {
2194 type Item = U;
2195
2196 fn poll_next(
2197 mut self: Pin<&mut Self>,
2198 cx: &mut std::task::Context<'_>,
2199 ) -> Poll<Option<Self::Item>> {
2200 let poll = self.inner.as_mut().poll_next(cx);
2201 if matches!(&poll, Poll::Ready(None))
2202 || matches!(&poll, Poll::Ready(Some(item)) if item.err().is_some())
2203 {
2204 self.release();
2205 }
2206 poll
2207 }
2208}
2209
2210impl<U: Data + MaybeError> AsyncEngineContextProvider for OccupancyTrackedStream<U> {
2211 fn context(&self) -> Arc<dyn AsyncEngineContext> {
2212 self.inner.context()
2213 }
2214}
2215
2216impl<U: Data + MaybeError> crate::engine::AsyncEngineStream<U> for OccupancyTrackedStream<U> {}
2217
2218#[cfg(test)]
2219mod tests {
2220 use super::*;
2221 use std::sync::atomic::Ordering;
2222
2223 use crate::{
2224 DistributedRuntime, Runtime,
2225 distributed::DistributedConfig,
2226 error::DynamoError,
2227 pipeline::{
2228 RequestStream, ResponseStream,
2229 context::{Context, Controller},
2230 network::egress::route_span,
2231 },
2232 };
2233 use serde::{Deserialize, Serialize};
2234
2235 #[derive(Clone, Debug, Deserialize, Serialize)]
2236 struct TestResponse {
2237 error: Option<DynamoError>,
2238 }
2239
2240 impl MaybeError for TestResponse {
2241 fn from_err(err: impl std::error::Error + 'static) -> Self {
2242 Self {
2243 error: Some(DynamoError::from(
2244 Box::new(err) as Box<dyn std::error::Error + 'static>
2245 )),
2246 }
2247 }
2248
2249 fn err(&self) -> Option<DynamoError> {
2250 self.error.clone()
2251 }
2252 }
2253
2254 fn assert_cannot_connect(error: &anyhow::Error) {
2255 assert!(
2256 match_error_chain(error.as_ref(), &[ErrorType::CannotConnect], &[]),
2257 "expected CannotConnect error, got: {error}"
2258 );
2259 assert!(
2260 !match_error_chain(error.as_ref(), &[ErrorType::ResourceExhausted], &[]),
2261 "CannotConnect failure must not be masked as ResourceExhausted: {error}"
2262 );
2263 }
2264
2265 fn assert_not_cannot_connect(error: &anyhow::Error) {
2266 assert!(
2267 !match_error_chain(error.as_ref(), &[ErrorType::CannotConnect], &[]),
2268 "fallback-enabled failure must preserve its existing error semantics: {error}"
2269 );
2270 }
2271
2272 fn assert_unavailable(error: &anyhow::Error) {
2278 assert_eq!(
2279 route_span::error_type_from_chain(error.as_ref()),
2280 ErrorType::Unavailable,
2281 "no-permitted-fallback failure must be typed Unavailable, got: {error}"
2282 );
2283 assert_not_cannot_connect(error);
2284 }
2285
2286 struct StaticMultimodalCacheIndex {
2287 worker_id: u64,
2288 }
2289
2290 impl MultimodalCacheIndex for StaticMultimodalCacheIndex {
2291 fn workers_with_cache_key_hits(&self, cache_keys: &[String]) -> Vec<(u64, usize)> {
2292 vec![(self.worker_id, cache_keys.len())]
2293 }
2294
2295 fn remove_worker(&self, _worker_id: u64) {}
2296 }
2297
2298 #[test]
2299 fn router_mode_telemetry_labels_are_stable() {
2300 assert_eq!(RouterMode::RoundRobin.telemetry_label(), "round-robin");
2301 assert_eq!(RouterMode::Random.telemetry_label(), "random");
2302 assert_eq!(
2303 RouterMode::PowerOfTwoChoices.telemetry_label(),
2304 "power-of-two-choices"
2305 );
2306 assert_eq!(RouterMode::KV.telemetry_label(), "kv");
2307 assert_eq!(RouterMode::Direct.telemetry_label(), "direct");
2308 assert_eq!(RouterMode::LeastLoaded.telemetry_label(), "least-loaded");
2309 assert_eq!(
2310 RouterMode::DeviceAwareWeighted.telemetry_label(),
2311 "device-aware-weighted"
2312 );
2313 }
2314
2315 #[test]
2316 fn p2c_selects_lower_load_worker() {
2317 let state = RoutingOccupancyState::default();
2318 for _ in 0..10 {
2319 state.increment(1);
2320 }
2321 state.increment(2);
2322
2323 let result = p2c_select_from(&state, &[1, 2]);
2325 assert_eq!(result, 2);
2326 }
2327
2328 #[test]
2329 fn explicit_static_pickers_keep_policy_specific_state() {
2330 let (round_robin, random, configured) = route_pickers(RouterMode::KV);
2331 assert!(configured.is_none());
2332 assert_eq!(random.policy(), RoutePolicy::Random);
2333
2334 let candidates = CandidateView::Workers(&[10, 20, 30, 40]);
2335 random
2336 .select(candidates, RouteContext::default(), |_| 0)
2337 .expect("random selection must have a candidate");
2338 let selected = (0..2)
2339 .map(|_| {
2340 round_robin
2341 .select(candidates, RouteContext::default(), |_| 0)
2342 .expect("round-robin selection must have a candidate")
2343 .target
2344 .worker_id
2345 })
2346 .collect::<Vec<_>>();
2347 assert_eq!(selected, [10, 20]);
2348 }
2349
2350 #[test]
2351 fn p2c_selects_single_worker() {
2352 let state = RoutingOccupancyState::default();
2353 assert_eq!(p2c_select_from(&state, &[42]), 42);
2354 }
2355
2356 #[test]
2357 fn p2c_treats_missing_counts_as_zero() {
2358 let state = RoutingOccupancyState::default();
2359 for _ in 0..5 {
2360 state.increment(1);
2361 }
2362 let result = p2c_select_from(&state, &[1, 2]);
2364 assert_eq!(result, 2);
2365 }
2366
2367 #[test]
2368 fn p2c_returns_valid_worker_on_tie() {
2369 let state = RoutingOccupancyState::default();
2370 for _ in 0..3 {
2371 state.increment(1);
2372 state.increment(2);
2373 }
2374
2375 for _ in 0..100 {
2376 let result = p2c_select_from(&state, &[1, 2]);
2377 assert!(result == 1 || result == 2);
2378 }
2379 }
2380
2381 #[test]
2382 fn occupancy_permit_decrements_before_stream_creation() {
2383 let state = Arc::new(RoutingOccupancyState::default());
2384 let counter = state.increment(42);
2385 let permit = OccupancyPermit::from_counter(state.clone(), 42, counter);
2386 assert_eq!(state.load(42), 1);
2387 drop(permit);
2388 assert_eq!(state.load(42), 0);
2389 }
2390
2391 #[test]
2392 fn occupancy_tracked_stream_decrements_on_drop() {
2393 let state = Arc::new(RoutingOccupancyState::default());
2394 let counter = state.increment(7);
2395 let permit = OccupancyPermit::from_counter(state.clone(), 7, counter);
2396 let ctx: Arc<dyn AsyncEngineContext> = Arc::new(Controller::default());
2397 let stream = permit.into_tracked_stream(ResponseStream::new(
2398 Box::pin(tokio_stream::iter(vec![TestResponse { error: None }])),
2399 ctx,
2400 ));
2401 assert_eq!(state.load(7), 1);
2402 drop(stream);
2403 assert_eq!(state.load(7), 0);
2404 }
2405
2406 #[tokio::test]
2407 async fn occupancy_tracked_stream_decrements_on_completion() {
2408 let state = Arc::new(RoutingOccupancyState::default());
2409 let counter = state.increment(7);
2410 let permit = OccupancyPermit::from_counter(state.clone(), 7, counter);
2411 let ctx: Arc<dyn AsyncEngineContext> = Arc::new(Controller::default());
2412 let mut stream = permit.into_tracked_stream(ResponseStream::new(
2413 Box::pin(tokio_stream::iter(vec![TestResponse { error: None }])),
2414 ctx,
2415 ));
2416
2417 assert!(stream.next().await.unwrap().err().is_none());
2418 assert_eq!(state.load(7), 1);
2419 assert!(stream.next().await.is_none());
2420 assert_eq!(state.load(7), 0);
2421 drop(stream);
2422 assert_eq!(state.load(7), 0, "drop must not release twice after EOF");
2423 }
2424
2425 #[tokio::test]
2426 async fn occupancy_tracked_stream_releases_before_yielding_error() {
2427 let state = Arc::new(RoutingOccupancyState::default());
2428 let counter = state.increment(7);
2429 let permit = OccupancyPermit::from_counter(state.clone(), 7, counter);
2430 let ctx: Arc<dyn AsyncEngineContext> = Arc::new(Controller::default());
2431 let error = DynamoError::builder()
2432 .error_type(ErrorType::WorkerOverloaded)
2433 .message("worker queue full")
2434 .build();
2435 let mut stream = permit.into_tracked_stream(ResponseStream::new(
2436 Box::pin(tokio_stream::iter(vec![TestResponse {
2437 error: Some(error),
2438 }])),
2439 ctx,
2440 ));
2441
2442 let response = stream.next().await.expect("error response");
2443 assert!(response.err().is_some());
2444 assert_eq!(
2445 state.load(7),
2446 0,
2447 "occupancy must be released before retry observes the error"
2448 );
2449 }
2450
2451 #[test]
2452 fn same_id_readd_keeps_existing_permit_accounting() {
2453 let state = Arc::new(RoutingOccupancyState::default());
2454 state.retain(&[7]);
2455 let old_counter = state.increment(7);
2456 let old_permit = OccupancyPermit::from_counter(state.clone(), 7, old_counter);
2457
2458 state.retain(&[]);
2459 assert_eq!(state.load(7), 1);
2460 state.retain(&[7]);
2461 let new_permit = OccupancyPermit::acquire(state.clone(), 7);
2462 assert_eq!(state.load(7), 2);
2463
2464 drop(old_permit);
2465 assert_eq!(
2466 state.load(7),
2467 1,
2468 "re-added worker must retain the still-live request"
2469 );
2470 drop(new_permit);
2471 assert_eq!(state.load(7), 0);
2472 }
2473
2474 #[test]
2482 fn stream_incomplete_quarantines_the_worker() {
2483 let err = DynamoError::builder()
2484 .error_type(ErrorType::Backend(BackendError::StreamIncomplete))
2485 .message("stream ended before generation completed")
2486 .build();
2487 assert!(
2488 is_inhibited(&err),
2489 "StreamIncomplete must inhibit; it is migratable, so leaving it out \
2490 lets a retry reselect the failed worker"
2491 );
2492
2493 let cancelled = DynamoError::builder()
2494 .error_type(ErrorType::Cancelled)
2495 .message("client went away")
2496 .build();
2497 assert!(
2498 !is_inhibited(&cancelled),
2499 "client cancellation is not a worker fault"
2500 );
2501 }
2502
2503 #[test]
2504 fn p2c_lifecycle_tracks_inflight_counts_with_shared_tracker() {
2505 let state = Arc::new(RoutingOccupancyState::default());
2506 let mut permits = Vec::new();
2507 for _ in 0..5 {
2508 let selected = p2c_select_from(&state, &[1, 2]);
2509 permits.push(OccupancyPermit::acquire(state.clone(), selected));
2510 }
2511
2512 let total = state.load(1) + state.load(2);
2513 assert_eq!(total, 5, "5 in-flight requests should be tracked");
2514
2515 drop(permits);
2516 let total = state.load(1) + state.load(2);
2517 assert_eq!(total, 0, "All guards dropped, counts should be 0");
2518 }
2519
2520 #[test]
2521 fn p2c_never_selects_dominated_worker() {
2522 let state = RoutingOccupancyState::default();
2523 for _ in 0..100 {
2524 state.increment(3);
2525 }
2526
2527 let mut selected = [0u32; 3];
2528 for _ in 0..1000 {
2529 let result = p2c_select_from(&state, &[1, 2, 3]);
2530 match result {
2531 1 => selected[0] += 1,
2532 2 => selected[1] += 1,
2533 3 => selected[2] += 1,
2534 _ => panic!("unexpected worker id"),
2535 }
2536 }
2537 assert_eq!(
2538 selected[2], 0,
2539 "Worker 3 (load=100) should never be selected against load=0 workers, but got {} times",
2540 selected[2]
2541 );
2542 }
2543
2544 #[tokio::test]
2545 async fn least_loaded_selects_exact_min_and_tracks_counts() {
2546 let state = Arc::new(RoutingOccupancyState::default());
2547 state.increment(1);
2548 state.increment(1);
2549 state.increment(2);
2550
2551 let picker = RoutePicker::new(RoutePolicy::LeastLoaded);
2552 let (decision, counter) = state
2553 .select_and_admit(
2554 &picker,
2555 CandidateView::Workers(&[1, 2, 3]),
2556 RouteContext::default(),
2557 )
2558 .unwrap();
2559 let selected = decision.target.worker_id;
2560 assert_eq!(selected, 3);
2561
2562 let permit = OccupancyPermit::from_counter(
2563 state.clone(),
2564 selected,
2565 counter.expect("least-loaded selection must acquire a counter"),
2566 );
2567 assert_eq!(state.load(selected), 1);
2568 drop(permit);
2569 assert_eq!(state.load(selected), 0);
2570 }
2571
2572 #[tokio::test]
2573 async fn bidirectional_generate_bails_with_no_instances() {
2574 let rt = Runtime::from_current().unwrap();
2575 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2576 .await
2577 .unwrap();
2578 let ns = drt.namespace("test_bidi_no_instances".to_string()).unwrap();
2579 let component = ns.component("test_component".to_string()).unwrap();
2580 let endpoint = component.endpoint("test_endpoint".to_string());
2581 let client = endpoint.client().await.unwrap();
2582
2583 let router = PushRouter::<u64, TestResponse>::from_client(client, RouterMode::RoundRobin)
2584 .await
2585 .unwrap();
2586
2587 let input: ManyIn<u64> =
2588 Context::new(RequestStream::new(Box::pin(tokio_stream::iter(vec![
2589 1u64, 2u64,
2590 ]))));
2591 let result = router.generate(input).await;
2592 assert!(
2593 result.is_err(),
2594 "bidirectional generate must bail when no instances are registered"
2595 );
2596
2597 rt.shutdown();
2598 }
2599
2600 #[tokio::test]
2601 async fn bidirectional_generate_bails_for_kv_router_mode() {
2602 let rt = Runtime::from_current().unwrap();
2603 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2604 .await
2605 .unwrap();
2606 let ns = drt.namespace("test_bidi_kv_mode".to_string()).unwrap();
2607 let component = ns.component("test_component".to_string()).unwrap();
2608 let endpoint = component.endpoint("test_endpoint".to_string());
2609 let client = endpoint.client().await.unwrap();
2610
2611 let router = PushRouter::<u64, TestResponse>::from_client(client, RouterMode::KV)
2612 .await
2613 .unwrap();
2614
2615 let input: ManyIn<u64> =
2616 Context::new(RequestStream::new(Box::pin(tokio_stream::iter(vec![1u64]))));
2617 let result = router.generate(input).await;
2618 assert!(
2619 result.is_err(),
2620 "bidirectional generate must bail for RouterMode::KV"
2621 );
2622 let err_msg = format!("{:?}", result.unwrap_err());
2623 assert!(
2624 err_msg.contains("KV") || err_msg.contains("kv"),
2625 "error should mention KV: got {err_msg}"
2626 );
2627
2628 rt.shutdown();
2629 }
2630
2631 #[tokio::test]
2632 async fn bidirectional_generate_bails_for_direct_router_mode() {
2633 let rt = Runtime::from_current().unwrap();
2634 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2635 .await
2636 .unwrap();
2637 let ns = drt.namespace("test_bidi_direct_mode".to_string()).unwrap();
2638 let component = ns.component("test_component".to_string()).unwrap();
2639 let endpoint = component.endpoint("test_endpoint".to_string());
2640 let client = endpoint.client().await.unwrap();
2641
2642 let router = PushRouter::<u64, TestResponse>::from_client(client, RouterMode::Direct)
2643 .await
2644 .unwrap();
2645
2646 let input: ManyIn<u64> =
2647 Context::new(RequestStream::new(Box::pin(tokio_stream::iter(vec![1u64]))));
2648 let result = router.generate(input).await;
2649 assert!(
2650 result.is_err(),
2651 "bidirectional generate must bail for RouterMode::Direct"
2652 );
2653 let err_msg = format!("{:?}", result.unwrap_err());
2654 assert!(
2655 err_msg.contains("Direct") || err_msg.contains("direct"),
2656 "error should mention Direct: got {err_msg}"
2657 );
2658
2659 rt.shutdown();
2660 }
2661
2662 #[tokio::test]
2663 async fn bidirectional_generate_rejects_unsupported_load_aware_modes() {
2664 let rt = Runtime::from_current().unwrap();
2665 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2666 .await
2667 .unwrap();
2668 let ns = drt.namespace("test_bidi_load_aware".to_string()).unwrap();
2669 let component = ns.component("test_component".to_string()).unwrap();
2670
2671 for mode in [
2672 RouterMode::PowerOfTwoChoices,
2673 RouterMode::LeastLoaded,
2674 RouterMode::DeviceAwareWeighted,
2675 ] {
2676 let endpoint = component.endpoint("test_endpoint".to_string());
2677 let client = endpoint.client().await.unwrap();
2678 let router = PushRouter::<u64, TestResponse>::from_client(client, mode)
2679 .await
2680 .unwrap();
2681
2682 let input: ManyIn<u64> =
2683 Context::new(RequestStream::new(Box::pin(tokio_stream::iter(vec![1u64]))));
2684 let result = router.generate(input).await;
2685 assert!(
2686 result.is_err(),
2687 "bidirectional generate must reject {mode:?} (not yet supported)"
2688 );
2689 let err_msg = format!("{:?}", result.unwrap_err());
2690 assert!(
2691 err_msg.contains("not yet supported for bidirectional dispatch"),
2692 "error should explain the mode is unsupported, not 'no instances': got {err_msg}"
2693 );
2694 }
2695
2696 rt.shutdown();
2697 }
2698
2699 #[tokio::test]
2700 async fn least_loaded_peek_returns_available_worker_select_stays_none() {
2701 let rt = Runtime::from_current().unwrap();
2702 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2703 .await
2704 .unwrap();
2705 let ns = drt
2706 .namespace("test_least_loaded_router".to_string())
2707 .unwrap();
2708 let component = ns.component("test_component".to_string()).unwrap();
2709 let endpoint = component.endpoint("test_endpoint".to_string());
2710 let client = endpoint.client().await.unwrap();
2711
2712 endpoint.register_endpoint_instance().await.unwrap();
2713 client.wait_for_instances().await.unwrap();
2714
2715 let router = PushRouter::<u64, TestResponse>::from_client(client, RouterMode::LeastLoaded)
2716 .await
2717 .unwrap();
2718
2719 assert_eq!(router.select_next_worker(), None);
2722 assert!(
2723 router.peek_next_worker().is_some(),
2724 "LeastLoaded peek must return the available worker for disagg bootstrap"
2725 );
2726
2727 rt.shutdown();
2728 }
2729
2730 #[tokio::test]
2731 async fn exact_selection_releases_occupancy_when_preparation_fails() {
2732 let rt = Runtime::from_current().unwrap();
2733 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2734 .await
2735 .unwrap();
2736 let ns = drt
2737 .namespace("test_exact_prepare_failure".to_string())
2738 .unwrap();
2739 let component = ns.component("test_component".to_string()).unwrap();
2740 let endpoint = component.endpoint("test_endpoint".to_string());
2741 let client = endpoint.client().await.unwrap();
2742 endpoint.register_endpoint_instance().await.unwrap();
2743 let worker_id = client.wait_for_instances().await.unwrap()[0].id();
2744
2745 let router = PushRouter::<u64, TestResponse>::from_client(client, RouterMode::LeastLoaded)
2746 .await
2747 .unwrap();
2748 let state = router.occupancy_state.clone().unwrap();
2749 let result = router
2750 .select_and_dispatch_exact(SingleIn::new(42), None, |_, _| {
2751 Err::<(), _>(anyhow::anyhow!("metadata preparation failed"))
2752 })
2753 .await;
2754
2755 assert!(result.is_err());
2756 assert_eq!(
2757 state.load(worker_id),
2758 0,
2759 "preparation failure must release the selected worker"
2760 );
2761 rt.shutdown();
2762 }
2763
2764 #[tokio::test]
2765 async fn exact_dispatch_revalidates_overload_after_preparation() {
2766 let rt = Runtime::from_current().unwrap();
2767 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2768 .await
2769 .unwrap();
2770 let ns = drt
2771 .namespace("test_exact_overload_revalidation".to_string())
2772 .unwrap();
2773 let component = ns.component("test_component".to_string()).unwrap();
2774 let endpoint = component.endpoint("test_endpoint".to_string());
2775 let client = endpoint.client().await.unwrap();
2776 endpoint.register_endpoint_instance().await.unwrap();
2777 let worker_id = client.wait_for_instances().await.unwrap()[0].id();
2778
2779 let router =
2780 PushRouter::<u64, TestResponse>::from_client(client.clone(), RouterMode::LeastLoaded)
2781 .await
2782 .unwrap();
2783 let state = router.occupancy_state.clone().unwrap();
2784 let result = router
2785 .select_and_dispatch_exact(SingleIn::new(42), Some(worker_id), |_, worker_id| {
2786 client.set_overloaded_instances(&[worker_id]);
2787 Ok(())
2788 })
2789 .await;
2790
2791 assert!(result.is_err());
2792 assert_eq!(
2793 state.load(worker_id),
2794 0,
2795 "validation failure must release the selected worker"
2796 );
2797 rt.shutdown();
2798 }
2799
2800 #[tokio::test]
2801 async fn transport_resolution_precedes_stale_overload_check() {
2802 let rt = Runtime::from_current().unwrap();
2803 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2804 .await
2805 .unwrap();
2806 let endpoint = drt
2807 .namespace("test_transport_precedes_stale_overload".to_string())
2808 .unwrap()
2809 .component("test_component".to_string())
2810 .unwrap()
2811 .endpoint("test_endpoint".to_string());
2812 let client = endpoint.client().await.unwrap();
2813 let stale_id = 99999;
2814 client.override_instance_avail(vec![stale_id]);
2815 client.set_overloaded_instances(&[stale_id]);
2816 let router = PushRouter::<u64, TestResponse>::from_client(client, RouterMode::RoundRobin)
2817 .await
2818 .unwrap();
2819
2820 let unary_error = router
2821 .direct(SingleIn::new(42), stale_id)
2822 .await
2823 .unwrap_err();
2824 assert_cannot_connect(&unary_error);
2825
2826 let input: ManyIn<u64> =
2827 Context::new(RequestStream::new(Box::pin(tokio_stream::iter(vec![1u64]))));
2828 let bidirectional_error = router
2829 .bidirectional_dispatch(stale_id, input)
2830 .await
2831 .unwrap_err();
2832 assert_not_cannot_connect(&bidirectional_error);
2833 assert!(
2834 !match_error_chain(
2835 bidirectional_error.as_ref(),
2836 &[ErrorType::ResourceExhausted],
2837 &[]
2838 ),
2839 "transport resolution must precede the stale overload check: {bidirectional_error}"
2840 );
2841
2842 rt.shutdown();
2843 }
2844
2845 #[tokio::test]
2846 async fn selected_overloaded_worker_is_rejected_before_dispatch() {
2847 const TEST_RECONCILE_INTERVAL: std::time::Duration = std::time::Duration::from_millis(50);
2848
2849 let rt = Runtime::from_current().unwrap();
2850 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2851 .await
2852 .unwrap();
2853 let ns = drt
2854 .namespace("test_selected_overloaded_worker_rejected".to_string())
2855 .unwrap();
2856 let component = ns.component("test_component".to_string()).unwrap();
2857 let endpoint = component.endpoint("test_endpoint".to_string());
2858 let client = Client::with_reconcile_interval(endpoint.clone(), TEST_RECONCILE_INTERVAL)
2859 .await
2860 .unwrap();
2861
2862 endpoint.register_endpoint_instance().await.unwrap();
2863 let instances = client.wait_for_instances().await.unwrap();
2864 let worker_id = instances[0].id();
2865
2866 for _ in 0..10 {
2867 if client.instance_ids_avail().contains(&worker_id) {
2868 break;
2869 }
2870 tokio::time::sleep(TEST_RECONCILE_INTERVAL).await;
2871 }
2872 assert!(
2873 client.instance_ids_avail().contains(&worker_id),
2874 "worker should be routable before marking it overloaded"
2875 );
2876
2877 client.set_overloaded_instances(&[worker_id]);
2878 let router = PushRouter::<u64, TestResponse>::from_client(client, RouterMode::RoundRobin)
2879 .await
2880 .unwrap();
2881
2882 let result = router.generate(SingleIn::new(42u64)).await;
2883 assert!(result.is_err());
2884 let msg = format!("{}", result.unwrap_err());
2885 assert!(
2891 msg.contains("All workers are busy"),
2892 "expected empty-free-pool rejection, got: {msg}"
2893 );
2894
2895 rt.shutdown();
2896 }
2897
2898 #[tokio::test]
2899 async fn direct_within_rejects_overloaded_constrained_target() {
2900 const TEST_RECONCILE_INTERVAL: std::time::Duration = std::time::Duration::from_secs(3600);
2901
2902 let rt = Runtime::from_current().unwrap();
2903 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2904 .await
2905 .unwrap();
2906 let ns = drt
2907 .namespace("test_direct_within_overload_rejection".to_string())
2908 .unwrap();
2909 let component = ns.component("test_component".to_string()).unwrap();
2910 let endpoint = component.endpoint("test_endpoint".to_string());
2911 let client = Client::with_reconcile_interval(endpoint.clone(), TEST_RECONCILE_INTERVAL)
2912 .await
2913 .unwrap();
2914
2915 endpoint.register_endpoint_instance().await.unwrap();
2916 let worker_id = client.wait_for_instances().await.unwrap()[0].id();
2917 client.set_overloaded_instances(&[worker_id]);
2918
2919 let router = PushRouter::<u64, TestResponse>::from_client(client, RouterMode::RoundRobin)
2920 .await
2921 .unwrap();
2922 let allowed = HashSet::from([worker_id]);
2923 let error = router
2924 .direct_within(SingleIn::new(42), worker_id, Some(&allowed))
2925 .await
2926 .unwrap_err();
2927
2928 assert!(match_error_chain(
2932 error.as_ref(),
2933 &[ErrorType::WorkerOverloaded],
2934 &[]
2935 ));
2936 assert!(
2937 error.to_string().contains("Selected worker is overloaded"),
2938 "expected overload rejection, got: {error}"
2939 );
2940
2941 rt.shutdown();
2942 }
2943
2944 #[tokio::test]
2945 async fn no_workers_is_reported_as_unavailable() {
2946 let rt = Runtime::from_current().unwrap();
2947 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2948 .await
2949 .unwrap();
2950 let ns = drt
2951 .namespace("test_no_workers_unavailable".to_string())
2952 .unwrap();
2953 let component = ns.component("test_component".to_string()).unwrap();
2954 let endpoint = component.endpoint("test_endpoint".to_string());
2955 let client = endpoint.client().await.unwrap();
2956 let router = PushRouter::<u64, TestResponse>::from_client(client, RouterMode::RoundRobin)
2957 .await
2958 .unwrap();
2959
2960 let error = router.generate(SingleIn::new(42)).await.unwrap_err();
2961 assert!(match_error_chain(
2962 error.as_ref(),
2963 &[ErrorType::Unavailable],
2964 &[]
2965 ));
2966
2967 rt.shutdown();
2968 }
2969
2970 #[tokio::test]
2971 async fn round_robin_excludes_overloaded_workers_from_candidates() {
2972 const TEST_RECONCILE_INTERVAL: std::time::Duration = std::time::Duration::from_secs(3600);
2978
2979 let rt = Runtime::from_current().unwrap();
2980 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2981 .await
2982 .unwrap();
2983 let ns = drt
2984 .namespace("test_round_robin_excludes_overloaded".to_string())
2985 .unwrap();
2986 let component = ns.component("test_component".to_string()).unwrap();
2987 let endpoint = component.endpoint("test_endpoint".to_string());
2988 let client = Client::with_reconcile_interval(endpoint.clone(), TEST_RECONCILE_INTERVAL)
2989 .await
2990 .unwrap();
2991
2992 endpoint.register_endpoint_instance().await.unwrap();
2993 let instances = client.wait_for_instances().await.unwrap();
2994 let real_id = instances[0].id();
2995 for _ in 0..50 {
2996 if client.instance_ids_avail().contains(&real_id) {
2997 break;
2998 }
2999 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
3000 }
3001
3002 client.override_instance_avail(vec![1, 2]);
3009 client.set_overloaded_instances(&[1]);
3010
3011 let router = PushRouter::<u64, TestResponse>::from_client(client, RouterMode::RoundRobin)
3012 .await
3013 .unwrap();
3014
3015 for _ in 0..6 {
3018 let selected = router
3019 .peek_next_worker()
3020 .expect("peek should succeed with a free worker");
3021 assert_eq!(
3022 selected, 2,
3023 "overloaded worker 1 must not appear in the candidate set"
3024 );
3025 }
3026
3027 rt.shutdown();
3028 }
3029
3030 #[tokio::test]
3031 async fn device_aware_weighted_peek_returns_available_worker_select_stays_none() {
3032 let rt = Runtime::from_current().unwrap();
3033 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
3034 .await
3035 .unwrap();
3036 let ns = drt
3037 .namespace("test_device_aware_router".to_string())
3038 .unwrap();
3039 let component = ns.component("test_component".to_string()).unwrap();
3040 let endpoint = component.endpoint("test_endpoint".to_string());
3041 let client = endpoint.client().await.unwrap();
3042
3043 endpoint.register_endpoint_instance().await.unwrap();
3044 client.wait_for_instances().await.unwrap();
3045
3046 let router =
3047 PushRouter::<u64, TestResponse>::from_client(client, RouterMode::DeviceAwareWeighted)
3048 .await
3049 .unwrap();
3050
3051 assert_eq!(router.select_next_worker(), None);
3054 assert!(
3055 router.peek_next_worker().is_some(),
3056 "DeviceAwareWeighted peek must return the available worker for disagg bootstrap"
3057 );
3058
3059 rt.shutdown();
3060 }
3061
3062 #[tokio::test]
3063 async fn device_aware_exact_selection_preserves_full_multimodal_cache_hit() {
3064 let rt = Runtime::from_current().unwrap();
3065 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
3066 .await
3067 .unwrap();
3068 let ns = drt
3069 .namespace("test_device_aware_affinity_cache".to_string())
3070 .unwrap();
3071 let component = ns.component("test_component".to_string()).unwrap();
3072 let endpoint = component.endpoint("test_endpoint".to_string());
3073 let client = endpoint.client().await.unwrap();
3074 endpoint.register_endpoint_instance().await.unwrap();
3075 let cache_worker = client.wait_for_instances().await.unwrap()[0].id();
3076
3077 let router = PushRouter::<u64, TestResponse>::from_client_with_state(
3078 client,
3079 RouterMode::DeviceAwareWeighted,
3080 None,
3081 Some(Arc::new(StaticMultimodalCacheIndex {
3082 worker_id: cache_worker,
3083 })),
3084 Some(Arc::new(|_| vec!["image-key".to_string()])),
3085 )
3086 .await
3087 .unwrap();
3088
3089 let selection = router.select_device_aware_and_reserve(&42, None).unwrap();
3090 assert_eq!(selection.worker_id(), cache_worker);
3091 assert!(
3092 selection.into_reservation().is_none(),
3093 "full cache hits bypass occupancy charging"
3094 );
3095
3096 rt.shutdown();
3097 }
3098
3099 #[tokio::test]
3101 async fn direct_dispatch_ignores_local_inhibition() {
3102 let rt = Runtime::from_current().unwrap();
3103 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
3104 .await
3105 .unwrap();
3106 let ns = drt
3107 .namespace("test_direct_bypasses_inhibition".to_string())
3108 .unwrap();
3109 let component = ns.component("test_component".to_string()).unwrap();
3110 let endpoint = component.endpoint("test_endpoint".to_string());
3111 let client = endpoint.client().await.unwrap();
3112 endpoint.register_endpoint_instance().await.unwrap();
3113 let instance_id = client.wait_for_instances().await.unwrap()[0].id();
3114
3115 let router = PushRouter::<u64, TestResponse>::from_client(client.clone(), RouterMode::KV)
3117 .await
3118 .unwrap();
3119
3120 client.report_instance_down(instance_id);
3121 assert!(
3122 !client.instance_ids_avail().contains(&instance_id),
3123 "precondition: worker should be locally inhibited"
3124 );
3125
3126 let result = router
3127 .direct_within_prepared(
3128 SingleIn::new(42),
3129 instance_id,
3130 None,
3131 |_, selected_instance_id| {
3132 assert_eq!(selected_instance_id, instance_id);
3133 Err::<(), _>(anyhow::anyhow!("direct prepare sentinel"))
3134 },
3135 )
3136 .await;
3137 let error = match result {
3138 Ok(_) => panic!("direct dispatch should reach request preparation"),
3139 Err(error) => error,
3140 };
3141 assert_eq!(error.to_string(), "direct prepare sentinel");
3142
3143 let missing_instance_id = instance_id.wrapping_add(1);
3144 let result = router
3145 .direct_within_prepared(SingleIn::new(42), missing_instance_id, None, |_, _| {
3146 Ok::<(), anyhow::Error>(())
3147 })
3148 .await;
3149 let error = match result {
3150 Ok(_) => panic!("direct dispatch should reject a worker absent from discovery"),
3151 Err(error) => error,
3152 };
3153 assert!(
3154 error
3155 .to_string()
3156 .contains(&format!("instance_id={missing_instance_id} not found")),
3157 "unexpected missing-worker error: {error}"
3158 );
3159
3160 rt.shutdown();
3161 }
3162
3163 #[tokio::test]
3167 async fn transport_resolution_falls_back_when_selected_instance_disappears() {
3168 let rt = Runtime::from_current().unwrap();
3169 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
3170 .await
3171 .unwrap();
3172 let ns = drt
3173 .namespace("test_transport_fallback".to_string())
3174 .unwrap();
3175 let component = ns.component("test_component".to_string()).unwrap();
3176 let endpoint = component.endpoint("test_endpoint".to_string());
3177 let client = endpoint.client().await.unwrap();
3178
3179 endpoint.register_endpoint_instance().await.unwrap();
3181 client.wait_for_instances().await.unwrap();
3182
3183 let real_id = client.instance_ids()[0];
3184
3185 let stale_id = real_id + 1000;
3189 client.override_instance_avail(vec![stale_id, real_id]);
3190
3191 let router =
3192 PushRouter::<u64, TestResponse>::from_client(client.clone(), RouterMode::RoundRobin)
3193 .await
3194 .unwrap();
3195
3196 let (resolved_id, _, _, _) = router
3200 .resolve_transport(stale_id, TransportFallback::Allow)
3201 .expect("normal routing should fall back from a stale worker");
3202 assert_eq!(resolved_id, real_id);
3203
3204 rt.shutdown();
3205 }
3206
3207 #[tokio::test]
3208 async fn prepared_dispatch_observes_worker_after_transport_fallback() {
3209 let rt = Runtime::from_current().unwrap();
3210 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
3211 .await
3212 .unwrap();
3213 let endpoint = drt
3214 .namespace("test_prepared_transport_fallback".to_string())
3215 .unwrap()
3216 .component("test_component".to_string())
3217 .unwrap()
3218 .endpoint("test_endpoint".to_string());
3219 let client = endpoint.client().await.unwrap();
3220 endpoint.register_endpoint_instance().await.unwrap();
3221 let real_id = client.wait_for_instances().await.unwrap()[0].id();
3222 let stale_id = real_id.wrapping_add(1);
3223 client.override_instance_avail(vec![stale_id, real_id]);
3224 let router = PushRouter::<u64, TestResponse>::from_client(client, RouterMode::LeastLoaded)
3225 .await
3226 .unwrap();
3227 let state = router.occupancy_state.clone().unwrap();
3228 state.increment(real_id);
3229 let state_for_prepare = state.clone();
3230 let observed = Arc::new(AtomicU64::new(0));
3231 let observed_for_prepare = observed.clone();
3232
3233 let _ = tokio::time::timeout(
3234 std::time::Duration::from_millis(100),
3235 router.select_and_dispatch(SingleIn::new(42), move |_, worker_id| {
3236 assert_eq!(state_for_prepare.load(stale_id), 0);
3237 assert_eq!(state_for_prepare.load(worker_id), 2);
3238 observed_for_prepare.store(worker_id, Ordering::Relaxed);
3239 Ok(())
3240 }),
3241 )
3242 .await;
3243
3244 assert_eq!(observed.load(Ordering::Relaxed), real_id);
3245 assert_eq!(state.load(real_id), 1);
3246 state.decrement(real_id);
3247 rt.shutdown();
3248 }
3249
3250 #[tokio::test]
3253 async fn transport_resolution_errors_when_no_instances_available() {
3254 let rt = Runtime::from_current().unwrap();
3255 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
3256 .await
3257 .unwrap();
3258 let ns = drt
3259 .namespace("test_transport_no_fallback".to_string())
3260 .unwrap();
3261 let component = ns.component("test_component".to_string()).unwrap();
3262 let endpoint = component.endpoint("test_endpoint".to_string());
3263 let monitor = tokio_util::sync::CancellationToken::new();
3271 let client = endpoint
3272 .client_with_cancellation(monitor.clone())
3273 .await
3274 .unwrap();
3275 monitor.cancel();
3276
3277 endpoint.register_endpoint_instance().await.unwrap();
3279 client.wait_for_instances().await.unwrap();
3280
3281 let router =
3282 PushRouter::<u64, TestResponse>::from_client(client.clone(), RouterMode::RoundRobin)
3283 .await
3284 .unwrap();
3285
3286 let stale_id = 99999;
3289 client.override_instance_avail(vec![stale_id]);
3290
3291 let request = SingleIn::new(42u64);
3292 let result = router.generate(request).await;
3293
3294 assert!(result.is_err());
3295 let error = result.unwrap_err();
3296 assert_unavailable(&error);
3297 let msg = error.to_string();
3298 assert!(
3299 msg.contains("not found") && msg.contains("no other instances available"),
3300 "Expected clear error about missing instance with no fallback, got: {msg}"
3301 );
3302
3303 rt.shutdown();
3304 }
3305
3306 #[tokio::test]
3307 async fn transport_resolution_honors_fallback_policy() {
3308 let rt = Runtime::from_current().unwrap();
3309 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
3310 .await
3311 .unwrap();
3312 let ns = drt
3313 .namespace("test_exact_transport_no_fallback".to_string())
3314 .unwrap();
3315 let component = ns.component("test_component".to_string()).unwrap();
3316 let endpoint = component.endpoint("test_endpoint".to_string());
3317 let monitor = tokio_util::sync::CancellationToken::new();
3325 let client = endpoint
3326 .client_with_cancellation(monitor.clone())
3327 .await
3328 .unwrap();
3329 monitor.cancel();
3330 endpoint.register_endpoint_instance().await.unwrap();
3331 let instances = client.wait_for_instances().await.unwrap();
3332 let real_id = instances[0].id();
3333
3334 let router =
3335 PushRouter::<u64, TestResponse>::from_client(client.clone(), RouterMode::RoundRobin)
3336 .await
3337 .unwrap();
3338 let stale_id = real_id.wrapping_add(1);
3339 client.override_instance_avail(vec![stale_id, real_id]);
3340
3341 assert!(
3342 router
3343 .resolve_transport(stale_id, TransportFallback::Allow)
3344 .is_ok(),
3345 "normal dispatch should preserve transport fallback"
3346 );
3347 let allowed = HashSet::from([real_id]);
3348 assert!(
3349 router
3350 .resolve_transport(stale_id, TransportFallback::Within(&allowed))
3351 .is_ok(),
3352 "constrained dispatch should fall back within the allowed worker set"
3353 );
3354 let disallowed = HashSet::new();
3355 let disallowed_error = router
3356 .resolve_transport(stale_id, TransportFallback::Within(&disallowed))
3357 .unwrap_err();
3358 assert_unavailable(&disallowed_error);
3359
3360 let exact_error = router
3361 .resolve_transport(stale_id, TransportFallback::Deny)
3362 .unwrap_err();
3363 assert_cannot_connect(&exact_error);
3364
3365 let second_stale_id = stale_id.wrapping_add(1);
3366 client.override_instance_avail(vec![stale_id, second_stale_id]);
3367 let stale_fallback_error = router
3368 .resolve_transport(stale_id, TransportFallback::Allow)
3369 .unwrap_err();
3370 assert_cannot_connect(&stale_fallback_error);
3371 assert!(
3372 stale_fallback_error
3373 .to_string()
3374 .contains("Fallback instance"),
3375 "expected fallback lookup failure, got: {stale_fallback_error}"
3376 );
3377 rt.shutdown();
3378 }
3379
3380 #[tokio::test]
3392 async fn watcher_dedup_guard_released_on_panic() {
3393 let endpoint_id = EndpointId {
3394 namespace: "panic-test-ns".to_string(),
3395 component: "panic-test-comp".to_string(),
3396 name: "panic-test-endpoint".to_string(),
3397 };
3398
3399 let map = ENDPOINT_WATCHER_ACTIVE.get_or_init(dashmap::DashMap::new);
3401 map.insert(endpoint_id.clone(), ());
3402
3403 let endpoint_id_clone = endpoint_id.clone();
3404 let join = tokio::spawn(async move {
3405 struct GuardRelease(EndpointId);
3407 impl Drop for GuardRelease {
3408 fn drop(&mut self) {
3409 if let Some(map) = ENDPOINT_WATCHER_ACTIVE.get() {
3410 map.remove(&self.0);
3411 }
3412 }
3413 }
3414 let _release = GuardRelease(endpoint_id_clone);
3415 panic!("simulated watcher-task panic");
3416 });
3417
3418 let result = join.await;
3419 assert!(result.is_err() && result.unwrap_err().is_panic());
3420 assert!(
3421 !map.contains_key(&endpoint_id),
3422 "Drop guard must release the dedup entry even on panic"
3423 );
3424 }
3425
3426 #[tokio::test]
3430 async fn watcher_dedup_guard_released_on_normal_exit() {
3431 let endpoint_id = EndpointId {
3432 namespace: "normal-test-ns".to_string(),
3433 component: "normal-test-comp".to_string(),
3434 name: "normal-test-endpoint".to_string(),
3435 };
3436
3437 let map = ENDPOINT_WATCHER_ACTIVE.get_or_init(dashmap::DashMap::new);
3438 map.insert(endpoint_id.clone(), ());
3439
3440 let endpoint_id_clone = endpoint_id.clone();
3441 tokio::spawn(async move {
3442 struct GuardRelease(EndpointId);
3443 impl Drop for GuardRelease {
3444 fn drop(&mut self) {
3445 if let Some(map) = ENDPOINT_WATCHER_ACTIVE.get() {
3446 map.remove(&self.0);
3447 }
3448 }
3449 }
3450 let _release = GuardRelease(endpoint_id_clone);
3451 })
3453 .await
3454 .unwrap();
3455
3456 assert!(!map.contains_key(&endpoint_id));
3457 }
3458
3459 #[tokio::test]
3460 async fn cache_cleanup_watcher_identity_includes_runtime() {
3461 let runtime = Runtime::from_current().unwrap();
3462 let first = DistributedRuntime::new(runtime.clone(), DistributedConfig::process_local())
3463 .await
3464 .unwrap();
3465 let second = DistributedRuntime::new(runtime.clone(), DistributedConfig::process_local())
3466 .await
3467 .unwrap();
3468 let endpoint = |distributed: &DistributedRuntime| {
3469 distributed
3470 .namespace("cache-watcher-identity".to_string())
3471 .unwrap()
3472 .component("workers".to_string())
3473 .unwrap()
3474 .endpoint("generate".to_string())
3475 };
3476 let first = RuntimeEndpointId::for_endpoint(&endpoint(&first));
3477 let second = RuntimeEndpointId::for_endpoint(&endpoint(&second));
3478
3479 assert_eq!(first.endpoint_id, second.endpoint_id);
3480 assert_ne!(first.connection_id, second.connection_id);
3481 assert_ne!(first, second);
3482
3483 runtime.shutdown();
3484 }
3485
3486 #[derive(Default)]
3491 struct RecordingDispatch {
3492 unary: std::sync::Mutex<Vec<(u64, String, Option<u64>)>>,
3493 bidi: std::sync::Mutex<Vec<(String, u64)>>,
3494 added: std::sync::Mutex<Vec<u64>>,
3495 removed: std::sync::Mutex<Vec<u64>>,
3496 }
3497
3498 impl RecordingDispatch {
3499 fn canned_stream() -> ManyOut<TestResponse> {
3500 let ctx: Arc<dyn AsyncEngineContext> = Arc::new(Controller::default());
3501 ResponseStream::new(
3502 Box::pin(tokio_stream::iter(vec![TestResponse { error: None }])),
3503 ctx,
3504 )
3505 }
3506 }
3507
3508 #[async_trait::async_trait]
3509 impl StreamingDispatch<u64, TestResponse> for RecordingDispatch {
3510 async fn generate(
3511 &self,
3512 request: SingleIn<AddressedRequest<u64>>,
3513 ) -> Result<ManyOut<TestResponse>, Error> {
3514 let (addressed, _ctx) = request.transfer(());
3515 let (payload, address, instance) = addressed.into_parts();
3516 self.unary
3517 .lock()
3518 .unwrap()
3519 .push((payload, address, instance.map(|i| i.id())));
3520 Ok(Self::canned_stream())
3521 }
3522
3523 async fn generate_bidirectional(
3524 &self,
3525 instance: Instance,
3526 address: String,
3527 _input: ManyIn<u64>,
3528 ) -> Result<ManyOut<TestResponse>, Error> {
3529 self.bidi.lock().unwrap().push((address, instance.id()));
3530 Ok(Self::canned_stream())
3531 }
3532
3533 async fn on_instance_removed(&self, id: &EndpointInstanceId) {
3534 self.removed.lock().unwrap().push(id.instance_id);
3535 }
3536
3537 async fn on_instance_added(&self, id: &EndpointInstanceId) {
3538 self.added.lock().unwrap().push(id.instance_id);
3539 }
3540 }
3541
3542 #[tokio::test]
3546 async fn from_client_with_dispatch_delivers_requests_and_lifecycle() {
3547 let rt = Runtime::from_current().unwrap();
3548 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
3549 .await
3550 .unwrap();
3551 let endpoint = drt
3552 .namespace("test_dispatch_seam".to_string())
3553 .unwrap()
3554 .component("test_component".to_string())
3555 .unwrap()
3556 .endpoint("test_endpoint".to_string());
3557 let client = endpoint.client().await.unwrap();
3558
3559 endpoint.register_endpoint_instance().await.unwrap();
3560 let instance_id = client.wait_for_instances().await.unwrap()[0].id();
3561
3562 let dispatch = Arc::new(RecordingDispatch::default());
3563 let router = PushRouter::<u64, TestResponse>::from_client_with_dispatch(
3564 client.clone(),
3565 RouterMode::RoundRobin,
3566 dispatch.clone(),
3567 )
3568 .await
3569 .unwrap();
3570
3571 let mut stream = router.generate(SingleIn::new(42u64)).await.unwrap();
3573 while stream.next().await.is_some() {}
3574 {
3575 let unary = dispatch.unary.lock().unwrap();
3576 assert_eq!(unary.len(), 1, "one unary dispatch expected");
3577 let (payload, address, dispatched) = &unary[0];
3578 assert_eq!(*payload, 42);
3579 assert_eq!(*dispatched, Some(instance_id));
3580 assert!(!address.is_empty(), "selected transport address expected");
3581 }
3582
3583 let input: ManyIn<u64> =
3585 Context::new(RequestStream::new(Box::pin(tokio_stream::iter(vec![
3586 1u64, 2u64,
3587 ]))));
3588 let mut stream = router.generate(input).await.unwrap();
3589 while stream.next().await.is_some() {}
3590 {
3591 let bidi = dispatch.bidi.lock().unwrap();
3592 assert_eq!(bidi.len(), 1, "one bidirectional dispatch expected");
3593 assert_eq!(bidi[0].1, instance_id);
3594 assert!(!bidi[0].0.is_empty());
3595 }
3596
3597 assert!(
3601 poll_until(|| dispatch.added.lock().unwrap().contains(&instance_id)).await,
3602 "on_instance_added (initial snapshot) not delivered to the supplied dispatch"
3603 );
3604
3605 endpoint.unregister_endpoint_instance().await.unwrap();
3606 assert!(
3607 poll_until(|| dispatch.removed.lock().unwrap().contains(&instance_id)).await,
3608 "on_instance_removed not delivered to the supplied dispatch"
3609 );
3610
3611 let adds_before = dispatch.added.lock().unwrap().len();
3613 endpoint.register_endpoint_instance().await.unwrap();
3614 assert!(
3615 poll_until(|| dispatch.added.lock().unwrap().len() > adds_before).await,
3616 "on_instance_added (re-registration) not delivered to the supplied dispatch"
3617 );
3618
3619 rt.shutdown();
3620 }
3621
3622 #[tokio::test]
3623 async fn admitted_dispatch_does_not_reject_the_load_it_just_booked() {
3624 const TEST_RECONCILE_INTERVAL: std::time::Duration = std::time::Duration::from_secs(3600);
3625
3626 let rt = Runtime::from_current().unwrap();
3627 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
3628 .await
3629 .unwrap();
3630 let endpoint = drt
3631 .namespace("test_admitted_dispatch".to_string())
3632 .unwrap()
3633 .component("test_component".to_string())
3634 .unwrap()
3635 .endpoint("test_endpoint".to_string());
3636 let client = Client::with_reconcile_interval(endpoint.clone(), TEST_RECONCILE_INTERVAL)
3637 .await
3638 .unwrap();
3639
3640 endpoint.register_endpoint_instance().await.unwrap();
3641 let instance_id = client.wait_for_instances().await.unwrap()[0].id();
3642 for _ in 0..50 {
3643 if client.instance_ids_avail().contains(&instance_id) {
3644 break;
3645 }
3646 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
3647 }
3648 assert!(client.instance_ids_avail().contains(&instance_id));
3649 let dispatch = Arc::new(RecordingDispatch::default());
3650 let router = PushRouter::<u64, TestResponse>::from_client_with_dispatch(
3651 client.clone(),
3652 RouterMode::KV,
3653 dispatch.clone(),
3654 )
3655 .await
3656 .unwrap();
3657
3658 client.set_overloaded_instances(&[instance_id]);
3659 let error = router
3660 .dispatch_exact(SingleIn::new(41), instance_id)
3661 .await
3662 .unwrap_err();
3663 assert!(match_error_chain(
3664 error.as_ref(),
3665 &[ErrorType::WorkerOverloaded],
3666 &[]
3667 ));
3668 assert!(dispatch.unary.lock().unwrap().is_empty());
3669
3670 let mut stream = router
3671 .dispatch_kv_admitted(SingleIn::new(42), instance_id)
3672 .await
3673 .unwrap();
3674 while stream.next().await.is_some() {}
3675 let unary = dispatch.unary.lock().unwrap();
3676 assert_eq!(unary.len(), 1);
3677 assert_eq!(unary[0].0, 42);
3678 assert!(!unary[0].1.is_empty());
3679 assert_eq!(unary[0].2, Some(instance_id));
3680 drop(unary);
3681
3682 rt.shutdown();
3683 }
3684
3685 async fn poll_until(mut pred: impl FnMut() -> bool) -> bool {
3688 for _ in 0..200 {
3689 if pred() {
3690 return true;
3691 }
3692 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
3693 }
3694 pred()
3695 }
3696}