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