1use std::{
5 collections::{HashMap, HashSet},
6 sync::{Arc, LazyLock, Mutex as StdMutex},
7 time::Duration,
8};
9
10use anyhow::Result;
11use arc_swap::ArcSwap;
12use futures::StreamExt;
13use tokio::time::Instant;
14
15use crate::component::{Endpoint, Instance};
16use crate::config::environment_names::runtime as env_runtime;
17use crate::discovery::{DiscoveryEvent, DiscoveryInstance, DiscoveryInstanceId};
18use crate::routing_policy::{RoutingOccupancyState, get_or_create_routing_occupancy_state};
19use crate::traits::DistributedRuntimeProvider;
20
21const DEFAULT_INHIBITED_DURATION_SECS: u64 = 5;
23
24const REQUEST_PATH_OVERLOAD_COOLDOWN: Duration = Duration::from_secs(1);
27
28static INHIBITED_DURATION: LazyLock<Duration> =
30 LazyLock::new(|| inhibited_duration_from_env(|name| std::env::var(name).ok()));
31
32fn inhibited_duration_from_env(mut lookup: impl FnMut(&str) -> Option<String>) -> Duration {
33 let seconds = match lookup(env_runtime::DYN_RUNTIME_INHIBITED_DURATION_SECS) {
34 None => DEFAULT_INHIBITED_DURATION_SECS,
35 Some(raw) => match raw.parse::<u64>() {
36 Ok(seconds) => seconds,
37 Err(err) => {
38 tracing::warn!(
39 value = raw,
40 %err,
41 "invalid {}; using the default of {} seconds",
42 env_runtime::DYN_RUNTIME_INHIBITED_DURATION_SECS,
43 DEFAULT_INHIBITED_DURATION_SECS,
44 );
45 DEFAULT_INHIBITED_DURATION_SECS
46 }
47 },
48 };
49 Duration::from_secs(seconds)
50}
51
52#[derive(Debug)]
60pub(crate) struct EndpointDiscoverySource {
61 instance_source: tokio::sync::watch::Receiver<Vec<Instance>>,
62 event_subscribers: StdMutex<Vec<tokio::sync::mpsc::UnboundedSender<DiscoveryEvent>>>,
63}
64
65pub(crate) struct DiscoveryEventReceiver {
66 receiver: tokio::sync::mpsc::UnboundedReceiver<DiscoveryEvent>,
67 _source: Arc<EndpointDiscoverySource>,
68}
69
70impl std::ops::Deref for DiscoveryEventReceiver {
71 type Target = tokio::sync::mpsc::UnboundedReceiver<DiscoveryEvent>;
72
73 fn deref(&self) -> &Self::Target {
74 &self.receiver
75 }
76}
77
78impl std::ops::DerefMut for DiscoveryEventReceiver {
79 fn deref_mut(&mut self) -> &mut Self::Target {
80 &mut self.receiver
81 }
82}
83
84impl EndpointDiscoverySource {
85 fn new(instance_source: tokio::sync::watch::Receiver<Vec<Instance>>) -> Self {
86 Self {
87 instance_source,
88 event_subscribers: StdMutex::new(Vec::new()),
89 }
90 }
91
92 fn instance_receiver(&self) -> tokio::sync::watch::Receiver<Vec<Instance>> {
93 self.instance_source.clone()
94 }
95
96 fn subscribe_events(&self) -> tokio::sync::mpsc::UnboundedReceiver<DiscoveryEvent> {
97 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
98 self.event_subscribers.lock().unwrap().push(tx);
99 rx
100 }
101
102 fn broadcast_event(&self, event: &DiscoveryEvent) {
103 let subscribers = &mut *self.event_subscribers.lock().unwrap();
104 subscribers.retain(|tx| tx.send(event.clone()).is_ok());
105 }
106}
107
108#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
109pub struct RoutingInstanceCounts {
110 pub discovered: usize,
111 pub routable: usize,
112 pub overloaded: usize,
113 pub free: usize,
115}
116
117#[derive(Clone, Debug)]
118pub(crate) struct RoutingInstances {
119 discovered_ids: Vec<u64>,
120 routable_ids: Vec<u64>,
121 monitor_overloaded_ids: HashSet<u64>,
122 request_overload_deadlines: HashMap<u64, Instant>,
123 next_request_overload_expiry: Option<Instant>,
124 overloaded_ids: HashSet<u64>,
125 free_ids: Vec<u64>,
126 routable_id_set: Arc<HashSet<u64>>,
127 availability_initialized: bool,
131}
132
133impl RoutingInstances {
134 fn new(discovered_ids: Vec<u64>) -> Self {
135 let availability_initialized = !discovered_ids.is_empty();
136 Self::from_parts(
137 discovered_ids.clone(),
138 discovered_ids,
139 HashSet::new(),
140 HashMap::new(),
141 availability_initialized,
142 )
143 }
144
145 fn from_parts(
146 mut discovered_ids: Vec<u64>,
147 mut routable_ids: Vec<u64>,
148 monitor_overloaded_ids: HashSet<u64>,
149 request_overload_deadlines: HashMap<u64, Instant>,
150 availability_initialized: bool,
151 ) -> Self {
152 discovered_ids.sort_unstable();
153 discovered_ids.dedup();
154 routable_ids.sort_unstable();
155 routable_ids.dedup();
156 let mut overloaded_ids = monitor_overloaded_ids.clone();
157 let next_request_overload_expiry = request_overload_deadlines.values().min().copied();
158 overloaded_ids.extend(request_overload_deadlines.keys().copied());
159 let free_ids = Self::derive_free_ids(&routable_ids, &overloaded_ids);
160 let routable_id_set = Arc::new(routable_ids.iter().copied().collect());
161 Self {
162 discovered_ids,
163 routable_ids,
164 monitor_overloaded_ids,
165 request_overload_deadlines,
166 next_request_overload_expiry,
167 overloaded_ids,
168 free_ids,
169 routable_id_set,
170 availability_initialized,
171 }
172 }
173
174 pub(crate) fn discovered_ids(&self) -> &[u64] {
175 &self.discovered_ids
176 }
177
178 pub(crate) fn routable_ids(&self) -> &[u64] {
179 &self.routable_ids
180 }
181
182 fn available_ids(&self) -> Option<Arc<HashSet<u64>>> {
183 self.availability_initialized
184 .then(|| Arc::clone(&self.routable_id_set))
185 }
186
187 pub(crate) fn free_ids(&self) -> &[u64] {
188 &self.free_ids
189 }
190
191 pub(crate) fn counts(&self) -> RoutingInstanceCounts {
192 RoutingInstanceCounts {
193 discovered: self.discovered_ids.len(),
194 routable: self.routable_ids.len(),
195 overloaded: self.overloaded_ids.len(),
196 free: self.free_ids.len(),
197 }
198 }
199
200 pub(crate) fn is_overloaded(&self, instance_id: u64) -> bool {
201 self.overloaded_ids.contains(&instance_id)
202 }
203
204 fn overloaded_ids(&self) -> Option<HashSet<u64>> {
205 if self.overloaded_ids.is_empty() {
206 return None;
207 }
208
209 Some(self.overloaded_ids.clone())
210 }
211
212 fn reconcile_discovered(&self, discovered_ids: Vec<u64>) -> Self {
213 let old_discovered_ids = self.discovered_ids.iter().copied().collect::<HashSet<_>>();
214 let new_discovered_ids = discovered_ids.iter().copied().collect::<HashSet<_>>();
215 let mut monitor_overloaded_ids = self.monitor_overloaded_ids.clone();
216 monitor_overloaded_ids
217 .retain(|id| !old_discovered_ids.contains(id) || new_discovered_ids.contains(id));
218 let mut request_overload_deadlines = self.request_overload_deadlines.clone();
219 request_overload_deadlines
220 .retain(|id, _| !old_discovered_ids.contains(id) || new_discovered_ids.contains(id));
221
222 let availability_initialized = self.availability_initialized || !discovered_ids.is_empty();
223 Self::from_parts(
224 discovered_ids.clone(),
225 discovered_ids,
226 monitor_overloaded_ids,
227 request_overload_deadlines,
228 availability_initialized,
229 )
230 }
231
232 fn report_instance_down(&self, instance_id: u64) -> Self {
233 let routable_ids: Vec<u64> = self
234 .routable_ids
235 .iter()
236 .copied()
237 .filter(|id| *id != instance_id)
238 .collect();
239
240 Self::from_parts(
241 self.discovered_ids.clone(),
242 routable_ids,
243 self.monitor_overloaded_ids.clone(),
244 self.request_overload_deadlines.clone(),
245 self.availability_initialized,
246 )
247 }
248
249 #[cfg(any(test, feature = "testing"))]
250 fn override_routable_ids(&self, routable_ids: Vec<u64>) -> Self {
251 Self::from_parts(
254 self.discovered_ids.clone(),
255 routable_ids,
256 self.monitor_overloaded_ids.clone(),
257 self.request_overload_deadlines.clone(),
258 self.availability_initialized,
259 )
260 }
261
262 fn set_overloaded(&self, overloaded_ids: HashSet<u64>) -> Self {
263 Self::from_parts(
264 self.discovered_ids.clone(),
265 self.routable_ids.clone(),
266 overloaded_ids,
267 HashMap::new(),
268 self.availability_initialized,
269 )
270 }
271
272 fn mark_overloaded_until(&self, instance_id: u64, deadline: Instant) -> Self {
276 let mut request_overload_deadlines = self.request_overload_deadlines.clone();
277 request_overload_deadlines.insert(instance_id, deadline);
278 Self::from_parts(
279 self.discovered_ids.clone(),
280 self.routable_ids.clone(),
281 self.monitor_overloaded_ids.clone(),
282 request_overload_deadlines,
283 self.availability_initialized,
284 )
285 }
286
287 fn has_expired_request_overload(&self, now: Instant) -> bool {
288 self.next_request_overload_expiry
289 .is_some_and(|deadline| deadline <= now)
290 }
291
292 fn clear_expired_request_overloads(&self, now: Instant) -> Self {
293 let mut request_overload_deadlines = self.request_overload_deadlines.clone();
294 request_overload_deadlines.retain(|_, deadline| *deadline > now);
295 Self::from_parts(
296 self.discovered_ids.clone(),
297 self.routable_ids.clone(),
298 self.monitor_overloaded_ids.clone(),
299 request_overload_deadlines,
300 self.availability_initialized,
301 )
302 }
303
304 fn clear_overloaded_for_removed(&self, removed_ids: &HashSet<u64>) -> Self {
305 let mut monitor_overloaded_ids = self.monitor_overloaded_ids.clone();
306 monitor_overloaded_ids.retain(|id| !removed_ids.contains(id));
307 let mut request_overload_deadlines = self.request_overload_deadlines.clone();
308 request_overload_deadlines.retain(|id, _| !removed_ids.contains(id));
309 Self::from_parts(
310 self.discovered_ids.clone(),
311 self.routable_ids.clone(),
312 monitor_overloaded_ids,
313 request_overload_deadlines,
314 self.availability_initialized,
315 )
316 }
317
318 fn derive_free_ids(routable_ids: &[u64], overloaded_ids: &HashSet<u64>) -> Vec<u64> {
319 if overloaded_ids.is_empty() {
320 return routable_ids.to_vec();
321 }
322
323 routable_ids
324 .iter()
325 .copied()
326 .filter(|id| !overloaded_ids.contains(id))
327 .collect()
328 }
329}
330
331#[derive(Debug)]
332struct RoutingInstancesState {
333 snapshot: ArcSwap<RoutingInstances>,
334 update_lock: StdMutex<()>,
335 instance_avail_tx: tokio::sync::watch::Sender<Vec<u64>>,
336}
337
338impl RoutingInstancesState {
339 fn new(discovered_ids: Vec<u64>) -> (Self, tokio::sync::watch::Receiver<Vec<u64>>) {
340 let snapshot = RoutingInstances::new(discovered_ids);
341 let (instance_avail_tx, instance_avail_rx) =
342 tokio::sync::watch::channel(snapshot.routable_ids().to_vec());
343 (
344 Self {
345 snapshot: ArcSwap::from_pointee(snapshot),
346 update_lock: StdMutex::new(()),
347 instance_avail_tx,
348 },
349 instance_avail_rx,
350 )
351 }
352
353 fn snapshot(&self) -> arc_swap::Guard<Arc<RoutingInstances>> {
354 let now = Instant::now();
355 let current = self.snapshot.load();
356 if !current.has_expired_request_overload(now) {
357 return current;
358 }
359 drop(current);
360
361 self.reconcile_expired_request_overloads(now);
362 self.snapshot.load()
363 }
364
365 fn reconcile_expired_request_overloads(&self, now: Instant) {
366 let _guard = self.update_lock.lock().unwrap();
367 let current = self.snapshot.load();
368 if !current.has_expired_request_overload(now) {
369 return;
370 }
371
372 let next = Arc::new(current.clear_expired_request_overloads(now));
373 self.snapshot.store(next);
374 }
375
376 fn update(
377 &self,
378 update: impl FnOnce(&RoutingInstances) -> RoutingInstances,
379 publish_routable_ids: bool,
380 ) -> Arc<RoutingInstances> {
381 let _guard = self.update_lock.lock().unwrap();
382 let current = self.snapshot.load();
383 let next = Arc::new(update(¤t));
384 self.snapshot.store(next.clone());
385 if publish_routable_ids {
386 self.publish_routable_ids(&next);
387 }
388 next
389 }
390
391 fn publish_routable_ids(&self, routing_instances: &RoutingInstances) {
392 let _ = self
393 .instance_avail_tx
394 .send(routing_instances.routable_ids().to_vec());
395 }
396
397 fn routable_ids(&self) -> Vec<u64> {
398 self.snapshot().routable_ids().to_vec()
399 }
400
401 fn available_ids(&self) -> Option<Arc<HashSet<u64>>> {
402 self.snapshot().available_ids()
403 }
404
405 fn free_ids(&self) -> Vec<u64> {
406 self.snapshot().free_ids.clone()
407 }
408
409 fn counts(&self) -> RoutingInstanceCounts {
410 self.snapshot().counts()
411 }
412
413 fn overloaded_ids(&self) -> Option<HashSet<u64>> {
414 self.snapshot().overloaded_ids()
415 }
416
417 fn report_instance_down(&self, instance_id: u64) {
418 self.update(|current| current.report_instance_down(instance_id), true);
419 }
420
421 fn set_overloaded_instances(&self, overloaded_instance_ids: &[u64]) -> bool {
422 let overloaded_ids = overloaded_instance_ids
423 .iter()
424 .copied()
425 .collect::<HashSet<_>>();
426 let _guard = self.update_lock.lock().unwrap();
427 let current = self.snapshot.load();
428 let next = Arc::new(current.set_overloaded(overloaded_ids));
429 let effective_changed = current.overloaded_ids != next.overloaded_ids;
430 let source_changed = current.monitor_overloaded_ids != next.monitor_overloaded_ids
431 || !current.request_overload_deadlines.is_empty();
432 if !source_changed {
433 return false;
434 }
435
436 self.snapshot.store(next);
437 effective_changed
438 }
439
440 fn mark_overloaded_immediate(&self, instance_id: u64) {
441 self.mark_overloaded_until(instance_id, Instant::now() + REQUEST_PATH_OVERLOAD_COOLDOWN);
442 }
443
444 fn mark_overloaded_until(&self, instance_id: u64, deadline: Instant) {
445 let _guard = self.update_lock.lock().unwrap();
446 let current = self.snapshot.load();
447 let next = Arc::new(current.mark_overloaded_until(instance_id, deadline));
448 self.snapshot.store(next);
449 }
450
451 fn overload_reconciliation_needed(&self) -> bool {
452 !self.snapshot().request_overload_deadlines.is_empty()
453 }
454
455 fn clear_overloaded_for_removed(&self, removed_instance_ids: &[u64]) {
456 if removed_instance_ids.is_empty() {
457 return;
458 }
459
460 let removed_ids = removed_instance_ids.iter().copied().collect::<HashSet<_>>();
461 self.update(
462 move |current| current.clear_overloaded_for_removed(&removed_ids),
463 false,
464 );
465 }
466
467 fn reconcile_discovered(&self, discovered_ids: Vec<u64>) -> Arc<RoutingInstances> {
468 self.update(
469 move |current| current.reconcile_discovered(discovered_ids),
470 true,
471 )
472 }
473
474 #[cfg(any(test, feature = "testing"))]
475 fn override_routable_ids(&self, ids: Vec<u64>) {
476 self.update(move |current| current.override_routable_ids(ids), true);
477 }
478}
479
480#[derive(Clone, Debug)]
481pub struct Client {
482 pub endpoint: Endpoint,
484 endpoint_discovery_source: Arc<EndpointDiscoverySource>,
486 pub instance_source: Arc<tokio::sync::watch::Receiver<Vec<Instance>>>,
488 routing_instances: Arc<RoutingInstancesState>,
490 instance_avail_owner: Arc<tokio::sync::watch::Receiver<Vec<u64>>>,
492 reconcile_interval: Duration,
496}
497
498impl Client {
499 pub(crate) async fn new(endpoint: Endpoint) -> Result<Self> {
501 Self::with_reconcile_interval(endpoint, *INHIBITED_DURATION).await
502 }
503
504 pub(crate) async fn with_cancellation(
509 endpoint: Endpoint,
510 cancel_token: tokio_util::sync::CancellationToken,
511 ) -> Result<Self> {
512 Self::with_reconcile_interval_and_cancellation(endpoint, *INHIBITED_DURATION, cancel_token)
513 .await
514 }
515
516 pub(crate) async fn with_reconcile_interval(
520 endpoint: Endpoint,
521 reconcile_interval: Duration,
522 ) -> Result<Self> {
523 let cancel_token = endpoint.drt().primary_token();
524 Self::with_reconcile_interval_and_cancellation(endpoint, reconcile_interval, cancel_token)
525 .await
526 }
527
528 pub(crate) async fn with_reconcile_interval_and_cancellation(
541 endpoint: Endpoint,
542 reconcile_interval: Duration,
543 cancel_token: tokio_util::sync::CancellationToken,
544 ) -> Result<Self> {
545 tracing::trace!(
546 "Client::new_dynamic: Creating dynamic client for endpoint: {}",
547 endpoint.id()
548 );
549 let endpoint_discovery_source =
550 Self::get_or_create_dynamic_discovery_source(&endpoint).await?;
551 let instance_source = Arc::new(endpoint_discovery_source.instance_receiver());
552
553 let initial_ids: Vec<u64> = instance_source
558 .borrow()
559 .iter()
560 .map(|instance| instance.id())
561 .collect();
562 let (routing_instances, instance_avail_owner) = RoutingInstancesState::new(initial_ids);
563 let client = Client {
564 endpoint: endpoint.clone(),
565 endpoint_discovery_source,
566 instance_source: instance_source.clone(),
567 routing_instances: Arc::new(routing_instances),
568 instance_avail_owner: Arc::new(instance_avail_owner),
569 reconcile_interval,
570 };
571 client.monitor_instance_source_with_cancellation(cancel_token, true);
572 Ok(client)
573 }
574
575 pub fn instances(&self) -> Vec<Instance> {
577 self.instance_source.borrow().clone()
578 }
579
580 pub fn instance_ids(&self) -> Vec<u64> {
581 self.instances().into_iter().map(|ep| ep.id()).collect()
582 }
583
584 pub fn is_instance_discovered(&self, instance_id: u64) -> bool {
586 self.routing_instances
587 .snapshot()
588 .discovered_ids()
589 .binary_search(&instance_id)
590 .is_ok()
591 }
592
593 pub fn instance_ids_avail(&self) -> Vec<u64> {
594 self.routing_instances.routable_ids()
595 }
596
597 pub fn instance_ids_free(&self) -> Vec<u64> {
600 self.routing_instances.free_ids()
601 }
602
603 pub(crate) fn routing_instances(&self) -> arc_swap::Guard<Arc<RoutingInstances>> {
604 self.routing_instances.snapshot()
605 }
606
607 pub fn routing_instance_counts(&self) -> RoutingInstanceCounts {
608 self.routing_instances.counts()
609 }
610
611 pub fn instance_avail_watcher(&self) -> tokio::sync::watch::Receiver<Vec<u64>> {
613 self.instance_avail_owner.as_ref().clone()
614 }
615
616 pub fn with_admitted_instances(
625 &self,
626 admitted_ids: tokio::sync::watch::Receiver<Vec<u64>>,
627 ) -> Self {
628 self.with_admitted_instances_and_cancellation(
629 admitted_ids,
630 self.endpoint.drt().primary_token(),
631 )
632 }
633
634 pub fn with_admitted_instances_and_cancellation(
637 &self,
638 mut admitted_ids: tokio::sync::watch::Receiver<Vec<u64>>,
639 cancel_token: tokio_util::sync::CancellationToken,
640 ) -> Self {
641 let mut endpoint_instances = self.instance_source.as_ref().clone();
642 let initial = Self::filter_admitted_instances(
643 endpoint_instances.borrow().as_slice(),
644 admitted_ids.borrow().as_slice(),
645 );
646 let initial_ids = initial.iter().map(Instance::id).collect::<Vec<_>>();
647 let (instance_tx, instance_rx) = tokio::sync::watch::channel(initial);
648 let updater_cancel = cancel_token.clone();
649
650 tokio::spawn(async move {
651 loop {
652 tokio::select! {
653 _ = updater_cancel.cancelled() => break,
654 _ = instance_tx.closed() => break,
655 result = endpoint_instances.changed() => {
656 if result.is_err() {
657 break;
658 }
659 }
660 result = admitted_ids.changed() => {
661 if result.is_err() {
662 break;
663 }
664 }
665 }
666
667 let next = Self::filter_admitted_instances(
668 endpoint_instances.borrow_and_update().as_slice(),
669 admitted_ids.borrow_and_update().as_slice(),
670 );
671 let changed = *instance_tx.borrow() != next;
672 if changed && instance_tx.send(next).is_err() {
673 break;
674 }
675 }
676 });
677
678 let (routing_instances, instance_avail_owner) = RoutingInstancesState::new(initial_ids);
679 let client = Self {
680 endpoint: self.endpoint.clone(),
681 endpoint_discovery_source: self.endpoint_discovery_source.clone(),
682 instance_source: Arc::new(instance_rx),
683 routing_instances: Arc::new(routing_instances),
684 instance_avail_owner: Arc::new(instance_avail_owner),
685 reconcile_interval: self.reconcile_interval,
686 };
687 client.monitor_instance_source_with_cancellation(cancel_token, false);
688 client
689 }
690
691 fn filter_admitted_instances(instances: &[Instance], admitted_ids: &[u64]) -> Vec<Instance> {
692 if admitted_ids.is_empty() {
693 return Vec::new();
694 }
695
696 let admitted = admitted_ids.iter().copied().collect::<HashSet<_>>();
697 instances
698 .iter()
699 .filter(|instance| admitted.contains(&instance.id()))
700 .cloned()
701 .collect()
702 }
703
704 pub(crate) fn subscribe_discovery_events(&self) -> DiscoveryEventReceiver {
709 DiscoveryEventReceiver {
710 receiver: self.endpoint_discovery_source.subscribe_events(),
711 _source: self.endpoint_discovery_source.clone(),
712 }
713 }
714
715 pub async fn wait_for_instances(&self) -> Result<Vec<Instance>> {
717 tracing::trace!(
718 "wait_for_instances: Starting wait for endpoint: {}",
719 self.endpoint.id()
720 );
721 let mut rx = self.instance_source.as_ref().clone();
722 let mut instances: Vec<Instance>;
724 loop {
725 instances = rx.borrow_and_update().to_vec();
726 if instances.is_empty() {
727 rx.changed().await?;
728 } else {
729 tracing::info!(
730 "wait_for_instances: Found {} instance(s) for endpoint: {}",
731 instances.len(),
732 self.endpoint.id()
733 );
734 break;
735 }
736 }
737 Ok(instances)
738 }
739
740 pub fn report_instance_down(&self, instance_id: u64) {
742 if self.reconcile_interval.is_zero() {
743 tracing::debug!(
744 instance_id,
745 "local worker inhibition is disabled; leaving instance routable"
746 );
747 return;
748 }
749
750 self.routing_instances.report_instance_down(instance_id);
751 tracing::debug!("inhibiting instance {instance_id}");
752 }
753
754 pub fn set_overloaded_instances(&self, overloaded_instance_ids: &[u64]) -> bool {
757 self.routing_instances
758 .set_overloaded_instances(overloaded_instance_ids)
759 }
760
761 pub fn overload_reconciliation_needed(&self) -> bool {
764 self.routing_instances.overload_reconciliation_needed()
765 }
766
767 pub fn mark_overloaded_immediate(&self, instance_id: u64) {
772 self.routing_instances
773 .mark_overloaded_immediate(instance_id);
774 tracing::debug!(
775 instance_id,
776 "marking instance overloaded with a bounded backpressure lease"
777 );
778 }
779
780 pub fn clear_overloaded_instances_for_removed(&self, removed_instance_ids: &[u64]) {
781 self.routing_instances
782 .clear_overloaded_for_removed(removed_instance_ids);
783 }
784
785 pub fn overloaded_instance_ids(&self) -> Option<HashSet<u64>> {
786 self.routing_instances.overloaded_ids()
787 }
788
789 pub fn available_instance_ids(&self) -> Option<Arc<HashSet<u64>>> {
797 self.routing_instances.available_ids()
798 }
799
800 fn monitor_instance_source_with_cancellation(
811 &self,
812 cancel_token: tokio_util::sync::CancellationToken,
813 prune_shared_occupancy: bool,
814 ) {
815 let reconcile_interval = self.reconcile_interval;
816 let endpoint = self.endpoint.clone();
817 let endpoint_discovery_source = self.endpoint_discovery_source.clone();
818 let routing_instances = self.routing_instances.clone();
819 let instance_source = self.instance_source.clone();
820 let endpoint_id = self.endpoint.id();
821 tokio::task::spawn(async move {
822 let mut rx = instance_source.as_ref().clone();
823 while !cancel_token.is_cancelled() {
824 let instance_ids: Vec<u64> = rx
825 .borrow_and_update()
826 .iter()
827 .map(|instance| instance.id())
828 .collect();
829
830 let snapshot = routing_instances.reconcile_discovered(instance_ids);
831
832 if prune_shared_occupancy {
834 let registry = endpoint.drt().routing_occupancy_states();
835 if let Ok(registry) = registry.try_lock()
836 && let Some(weak) = registry.get(&endpoint)
837 && let Some(state) = weak.upgrade()
838 {
839 state.retain(snapshot.discovered_ids());
840 }
841 }
842
843 tokio::select! {
844 _ = cancel_token.cancelled() => break,
845 _ = routing_instances.instance_avail_tx.closed() => break,
846 result = rx.changed() => {
847 if let Err(err) = result {
848 tracing::error!(
849 "monitor_instance_source: The Sender is dropped: {err}, endpoint={endpoint_id}",
850 );
851 cancel_token.cancel();
852 }
853 }
854 _ = tokio::time::sleep(reconcile_interval), if !reconcile_interval.is_zero() => {
855 tracing::trace!(
856 "monitor_instance_source: periodic reconciliation for endpoint={endpoint_id}",
857 );
858 }
859 }
860 }
861 drop(endpoint_discovery_source);
862 });
863 }
864
865 #[cfg(any(test, feature = "testing"))]
867 pub fn override_discovered_instances(&self, ids: Vec<u64>) {
868 self.reconcile_discovered_instances(ids);
869 }
870
871 #[cfg(any(test, feature = "testing"))]
873 pub fn override_instance_avail(&self, ids: Vec<u64>) {
874 self.routing_instances.override_routable_ids(ids);
875 }
876
877 fn reconcile_discovered_instances(&self, discovered_ids: Vec<u64>) -> Arc<RoutingInstances> {
878 self.routing_instances.reconcile_discovered(discovered_ids)
879 }
880
881 async fn get_or_create_dynamic_discovery_source(
882 endpoint: &Endpoint,
883 ) -> Result<Arc<EndpointDiscoverySource>> {
884 let drt = endpoint.drt();
885 let sources = drt.endpoint_discovery_sources();
886 let mut sources = sources.lock().await;
887
888 if let Some(source) = sources.get(endpoint) {
889 if let Some(source) = source.upgrade() {
890 return Ok(source);
891 } else {
892 sources.remove(endpoint);
893 }
894 }
895
896 let discovery = drt.discovery();
897 let discovery_query = crate::discovery::DiscoveryQuery::Endpoint {
898 namespace: endpoint.component.namespace.name.clone(),
899 component: endpoint.component.name.clone(),
900 endpoint: endpoint.name.clone(),
901 };
902
903 let mut discovery_stream = discovery
904 .list_and_watch(discovery_query.clone(), None)
905 .await?;
906 let (watch_tx, watch_rx) = tokio::sync::watch::channel(vec![]);
907 let discovery_source = Arc::new(EndpointDiscoverySource::new(watch_rx));
908
909 let secondary = endpoint.component.drt.runtime().secondary().clone();
910 let discovery_source_task = Arc::downgrade(&discovery_source);
911
912 secondary.spawn(async move {
913 tracing::trace!("endpoint_watcher: Starting for discovery query: {:?}", discovery_query);
914 let mut map: HashMap<u64, Instance> = HashMap::new();
915
916 loop {
917 let discovery_event = tokio::select! {
918 _ = watch_tx.closed() => {
919 break;
920 }
921 discovery_event = discovery_stream.next() => {
922 match discovery_event {
923 Some(Ok(event)) => {
924 event
925 },
926 Some(Err(e)) => {
927 tracing::error!("endpoint_watcher: discovery stream error: {}; shutting down for discovery query: {:?}", e, discovery_query);
928 break;
929 }
930 None => {
931 break;
932 }
933 }
934 }
935 };
936
937 if let Some(discovery_source) = discovery_source_task.upgrade() {
938 discovery_source.broadcast_event(&discovery_event);
939 }
940
941 match discovery_event {
942 DiscoveryEvent::Added(DiscoveryInstance::Endpoint(instance)) => {
943 map.insert(instance.instance_id, instance);
944 }
945 DiscoveryEvent::Added(_) => {}
946 DiscoveryEvent::ModelTaintsUpdated(_) => {}
947 DiscoveryEvent::Removed(id) => {
948 if let DiscoveryInstanceId::Endpoint(endpoint_id) = id {
949 map.remove(&endpoint_id.instance_id);
950 }
951 }
952 }
953
954 let instances: Vec<Instance> = map.values().cloned().collect();
955 if watch_tx.send(instances).is_err() {
956 break;
957 }
958 }
959 let _ = watch_tx.send(vec![]);
960 });
961
962 sources.insert(endpoint.clone(), Arc::downgrade(&discovery_source));
963 Ok(discovery_source)
964 }
965}
966
967#[cfg(test)]
968mod tests {
969 use super::*;
970 use crate::{DistributedRuntime, Runtime, distributed::DistributedConfig};
971
972 async fn wait_for_discovery_event(
973 receiver: &mut DiscoveryEventReceiver,
974 predicate: impl Fn(&DiscoveryEvent) -> bool,
975 ) {
976 tokio::time::timeout(Duration::from_secs(1), async {
977 loop {
978 let event = receiver.recv().await.expect("discovery event feed closed");
979 if predicate(&event) {
980 return;
981 }
982 }
983 })
984 .await
985 .expect("expected discovery event was not received");
986 }
987
988 async fn wait_for_watch_state<T>(
989 receiver: &mut tokio::sync::watch::Receiver<Vec<T>>,
990 predicate: impl Fn(&[T]) -> bool,
991 ) {
992 tokio::time::timeout(Duration::from_secs(1), async {
993 loop {
994 if predicate(receiver.borrow_and_update().as_slice()) {
995 return;
996 }
997 receiver
998 .changed()
999 .await
1000 .expect("instance availability feed closed");
1001 }
1002 })
1003 .await
1004 .expect("expected instance availability state was not observed");
1005 }
1006
1007 #[test]
1008 fn test_inhibited_duration_from_env() {
1009 assert_eq!(
1010 inhibited_duration_from_env(|_| None),
1011 Duration::from_secs(DEFAULT_INHIBITED_DURATION_SECS)
1012 );
1013 assert_eq!(
1014 inhibited_duration_from_env(|_| Some("17".to_string())),
1015 Duration::from_secs(17)
1016 );
1017 assert_eq!(
1018 inhibited_duration_from_env(|_| Some("0".to_string())),
1019 Duration::ZERO
1020 );
1021 assert_eq!(
1022 inhibited_duration_from_env(|_| Some("invalid".to_string())),
1023 Duration::from_secs(DEFAULT_INHIBITED_DURATION_SECS)
1024 );
1025 }
1026
1027 #[tokio::test]
1028 async fn dropping_last_client_releases_routing_state() {
1029 let rt = Runtime::from_current().unwrap();
1030 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1031 .await
1032 .unwrap();
1033 let endpoint = drt
1034 .namespace("test_client_lifecycle".to_string())
1035 .unwrap()
1036 .component("test_component".to_string())
1037 .unwrap()
1038 .endpoint("decode".to_string());
1039 let client = endpoint.client().await.unwrap();
1040 let routing_instances = Arc::downgrade(&client.routing_instances);
1041 let discovery_source = Arc::downgrade(&client.endpoint_discovery_source);
1042 let mut raw_watcher = client.instance_source.as_ref().clone();
1043 let mut watcher = client.instance_avail_watcher();
1044 let mut event_watcher = client.subscribe_discovery_events();
1045 raw_watcher.borrow_and_update();
1046 watcher.borrow_and_update();
1047
1048 drop(client);
1049 assert!(routing_instances.upgrade().is_some());
1050 assert!(discovery_source.upgrade().is_some());
1051
1052 endpoint.register_endpoint_instance().await.unwrap();
1053 wait_for_watch_state(&mut watcher, |instances| instances.len() == 1).await;
1054 wait_for_discovery_event(&mut event_watcher, |event| {
1055 matches!(event, DiscoveryEvent::Added(DiscoveryInstance::Endpoint(_)))
1056 })
1057 .await;
1058
1059 endpoint.unregister_endpoint_instance().await.unwrap();
1060 wait_for_watch_state(&mut watcher, |instances| instances.is_empty()).await;
1061 assert!(raw_watcher.borrow_and_update().is_empty());
1062 wait_for_discovery_event(&mut event_watcher, |event| {
1063 matches!(event, DiscoveryEvent::Removed(_))
1064 })
1065 .await;
1066
1067 drop(watcher);
1068
1069 tokio::time::timeout(Duration::from_secs(1), async {
1070 while routing_instances.strong_count() != 0 {
1071 tokio::task::yield_now().await;
1072 }
1073 })
1074 .await
1075 .expect("client monitor retained state after its last observer was dropped");
1076 assert!(discovery_source.upgrade().is_some());
1077
1078 endpoint.register_endpoint_instance().await.unwrap();
1079 wait_for_watch_state(&mut raw_watcher, |instances| instances.len() == 1).await;
1080 wait_for_discovery_event(&mut event_watcher, |event| {
1081 matches!(event, DiscoveryEvent::Added(DiscoveryInstance::Endpoint(_)))
1082 })
1083 .await;
1084 endpoint.unregister_endpoint_instance().await.unwrap();
1085 wait_for_watch_state(&mut raw_watcher, |instances| instances.is_empty()).await;
1086 wait_for_discovery_event(&mut event_watcher, |event| {
1087 matches!(event, DiscoveryEvent::Removed(_))
1088 })
1089 .await;
1090
1091 drop(event_watcher);
1092 tokio::time::timeout(Duration::from_secs(1), async {
1093 while discovery_source.strong_count() != 0 {
1094 tokio::task::yield_now().await;
1095 }
1096 })
1097 .await
1098 .expect("discovery event receiver retained its source after being dropped");
1099
1100 rt.shutdown();
1101 }
1102
1103 #[tokio::test]
1106 async fn test_instance_reconciliation() {
1107 const TEST_RECONCILE_INTERVAL: Duration = Duration::from_millis(100);
1108
1109 let rt = Runtime::from_current().unwrap();
1110 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1112 .await
1113 .unwrap();
1114 let ns = drt.namespace("test_reconciliation".to_string()).unwrap();
1115 let component = ns.component("test_component".to_string()).unwrap();
1116 let endpoint = component.endpoint("test_endpoint".to_string());
1117
1118 let client = Client::with_reconcile_interval(endpoint, TEST_RECONCILE_INTERVAL)
1120 .await
1121 .unwrap();
1122
1123 assert!(client.instance_ids_avail().is_empty());
1125
1126 client.override_instance_avail(vec![1, 2, 3]);
1129
1130 assert_eq!(client.instance_ids_avail(), vec![1u64, 2, 3]);
1131
1132 client.report_instance_down(2);
1134 assert_eq!(client.instance_ids_avail(), vec![1u64, 3]);
1135
1136 tokio::time::sleep(TEST_RECONCILE_INTERVAL + Duration::from_millis(50)).await;
1140
1141 assert!(
1143 client.instance_ids_avail().is_empty(),
1144 "After reconciliation, instance_avail should match instance_source"
1145 );
1146
1147 rt.shutdown();
1148 }
1149
1150 #[tokio::test]
1152 async fn test_zero_inhibited_duration_leaves_instance_routable() {
1153 let rt = Runtime::from_current().unwrap();
1154 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1155 .await
1156 .unwrap();
1157 let ns = drt
1158 .namespace("test_disabled_inhibition".to_string())
1159 .unwrap();
1160 let component = ns.component("test_component".to_string()).unwrap();
1161 let endpoint = component.endpoint("test_endpoint".to_string());
1162
1163 let client = Client::with_reconcile_interval(endpoint, Duration::ZERO)
1164 .await
1165 .unwrap();
1166
1167 client.override_instance_avail(vec![1, 2, 3]);
1168 client.report_instance_down(2);
1169
1170 assert_eq!(
1171 client.instance_ids_avail(),
1172 vec![1, 2, 3],
1173 "a zero inhibited duration should leave the reported instance routable"
1174 );
1175
1176 rt.shutdown();
1177 }
1178
1179 #[tokio::test]
1181 async fn test_report_instance_down() {
1182 let rt = Runtime::from_current().unwrap();
1183 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1185 .await
1186 .unwrap();
1187 let ns = drt.namespace("test_report_down".to_string()).unwrap();
1188 let component = ns.component("test_component".to_string()).unwrap();
1189 let endpoint = component.endpoint("test_endpoint".to_string());
1190
1191 let client = endpoint.client().await.unwrap();
1192
1193 client.override_instance_avail(vec![1, 2, 3]);
1195 assert_eq!(client.instance_ids_avail(), vec![1u64, 2, 3]);
1196
1197 client.report_instance_down(2);
1199
1200 let avail = client.instance_ids_avail();
1202 assert!(avail.contains(&1), "Instance 1 should still be available");
1203 assert!(
1204 !avail.contains(&2),
1205 "Instance 2 should be removed after report_instance_down"
1206 );
1207 assert!(avail.contains(&3), "Instance 3 should still be available");
1208
1209 rt.shutdown();
1210 }
1211
1212 #[tokio::test]
1213 async fn test_overloaded_instance_ids_returns_none_when_empty() {
1214 let rt = Runtime::from_current().unwrap();
1215 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1216 .await
1217 .unwrap();
1218 let ns = drt.namespace("test_overloaded_ids".to_string()).unwrap();
1219 let component = ns.component("test_component".to_string()).unwrap();
1220 let endpoint = component.endpoint("test_endpoint".to_string());
1221 let client = endpoint.client().await.unwrap();
1222
1223 assert_eq!(client.overloaded_instance_ids(), None);
1224
1225 assert!(client.set_overloaded_instances(&[7]));
1226 assert_eq!(client.overloaded_instance_ids(), Some(HashSet::from([7])));
1227 assert!(!client.set_overloaded_instances(&[7]));
1228
1229 assert!(client.set_overloaded_instances(&[]));
1230 assert_eq!(client.overloaded_instance_ids(), None);
1231 assert!(!client.set_overloaded_instances(&[]));
1232
1233 rt.shutdown();
1234 }
1235
1236 #[tokio::test(start_paused = true)]
1237 async fn request_path_overload_expires_without_monitor_update() {
1238 let rt = Runtime::from_current().unwrap();
1239 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1240 .await
1241 .unwrap();
1242 let client = drt
1243 .namespace("test_request_overload_expiry".to_string())
1244 .unwrap()
1245 .component("test_component".to_string())
1246 .unwrap()
1247 .endpoint("decode".to_string())
1248 .client()
1249 .await
1250 .unwrap();
1251
1252 client.mark_overloaded_immediate(7);
1253 assert_eq!(client.overloaded_instance_ids(), Some(HashSet::from([7])));
1254
1255 tokio::time::advance(REQUEST_PATH_OVERLOAD_COOLDOWN).await;
1256
1257 assert_eq!(client.overloaded_instance_ids(), None);
1258 assert!(!client.overload_reconciliation_needed());
1259 rt.shutdown();
1260 }
1261
1262 #[tokio::test]
1263 async fn request_path_expiry_preserves_monitor_overload() {
1264 let rt = Runtime::from_current().unwrap();
1265 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1266 .await
1267 .unwrap();
1268 let client = drt
1269 .namespace("test_request_expiry_preserves_monitor".to_string())
1270 .unwrap()
1271 .component("test_component".to_string())
1272 .unwrap()
1273 .endpoint("decode".to_string())
1274 .client()
1275 .await
1276 .unwrap();
1277 let now = Instant::now();
1278
1279 assert!(client.set_overloaded_instances(&[11]));
1280 client
1281 .routing_instances
1282 .mark_overloaded_until(7, now + Duration::from_secs(1));
1283 client
1284 .routing_instances
1285 .mark_overloaded_until(8, now + Duration::from_secs(2));
1286
1287 client
1288 .routing_instances
1289 .reconcile_expired_request_overloads(now + Duration::from_millis(1500));
1290
1291 assert_eq!(
1292 client.overloaded_instance_ids(),
1293 Some(HashSet::from([8, 11]))
1294 );
1295 assert!(client.overload_reconciliation_needed());
1296 rt.shutdown();
1297 }
1298
1299 #[tokio::test]
1300 async fn test_instance_reconciliation_preserves_overloaded_existing_instances() {
1301 const TEST_RECONCILE_INTERVAL: Duration = Duration::from_millis(50);
1302
1303 let rt = Runtime::from_current().unwrap();
1304 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1305 .await
1306 .unwrap();
1307 let ns = drt
1308 .namespace("test_overloaded_reconciliation".to_string())
1309 .unwrap();
1310 let component = ns.component("test_component".to_string()).unwrap();
1311 let endpoint = component.endpoint("test_endpoint".to_string());
1312
1313 let client = Client::with_reconcile_interval(endpoint.clone(), TEST_RECONCILE_INTERVAL)
1314 .await
1315 .unwrap();
1316 endpoint.register_endpoint_instance().await.unwrap();
1317 let instances = client.wait_for_instances().await.unwrap();
1318 let worker_id = instances[0].id();
1319
1320 for _ in 0..10 {
1321 if client.instance_ids_free().contains(&worker_id) {
1322 break;
1323 }
1324 tokio::time::sleep(TEST_RECONCILE_INTERVAL).await;
1325 }
1326 assert!(
1327 client.instance_ids_free().contains(&worker_id),
1328 "worker should be free after initial discovery reconciliation"
1329 );
1330
1331 client.set_overloaded_instances(&[worker_id]);
1332 assert!(
1333 client.instance_ids_free().is_empty(),
1334 "worker should be overloaded before periodic reconciliation"
1335 );
1336
1337 tokio::time::sleep(TEST_RECONCILE_INTERVAL + Duration::from_millis(50)).await;
1338
1339 assert!(
1340 client.instance_ids_free().is_empty(),
1341 "periodic reconciliation should not mark an existing overloaded worker free"
1342 );
1343
1344 rt.shutdown();
1345 }
1346
1347 #[tokio::test]
1348 async fn test_report_instance_down_preserves_overloaded_state() {
1349 const TEST_RECONCILE_INTERVAL: Duration = Duration::from_millis(50);
1350
1351 let rt = Runtime::from_current().unwrap();
1352 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1353 .await
1354 .unwrap();
1355 let ns = drt
1356 .namespace("test_report_down_preserves_overloaded".to_string())
1357 .unwrap();
1358 let component = ns.component("test_component".to_string()).unwrap();
1359 let endpoint = component.endpoint("test_endpoint".to_string());
1360
1361 let client = Client::with_reconcile_interval(endpoint.clone(), TEST_RECONCILE_INTERVAL)
1362 .await
1363 .unwrap();
1364 endpoint.register_endpoint_instance().await.unwrap();
1365 let instances = client.wait_for_instances().await.unwrap();
1366 let worker_id = instances[0].id();
1367
1368 for _ in 0..10 {
1369 if client.instance_ids_avail().contains(&worker_id) {
1370 break;
1371 }
1372 tokio::time::sleep(TEST_RECONCILE_INTERVAL).await;
1373 }
1374
1375 client.set_overloaded_instances(&[worker_id]);
1376 client.report_instance_down(worker_id);
1377
1378 assert!(
1379 !client.instance_ids_avail().contains(&worker_id),
1380 "reported-down worker should leave routable availability"
1381 );
1382 assert_eq!(
1383 client.routing_instance_counts().overloaded,
1384 1,
1385 "reported-down worker should remain overloaded while still discovered"
1386 );
1387 assert!(
1388 client.instance_ids_free().is_empty(),
1389 "reported-down overloaded worker should not become free"
1390 );
1391
1392 endpoint.unregister_endpoint_instance().await.unwrap();
1393 for _ in 0..10 {
1394 if client.routing_instance_counts().overloaded == 0 {
1395 break;
1396 }
1397 tokio::time::sleep(TEST_RECONCILE_INTERVAL).await;
1398 }
1399
1400 assert_eq!(
1401 client.routing_instance_counts().overloaded,
1402 0,
1403 "stable discovery removal should clear overloaded state"
1404 );
1405
1406 rt.shutdown();
1407 }
1408
1409 #[tokio::test]
1410 async fn test_instance_reconciliation_prunes_removed_overloaded_instances() {
1411 const TEST_RECONCILE_INTERVAL: Duration = Duration::from_millis(50);
1412
1413 let rt = Runtime::from_current().unwrap();
1414 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1415 .await
1416 .unwrap();
1417 let ns = drt
1418 .namespace("test_removed_overloaded_cleanup".to_string())
1419 .unwrap();
1420 let component = ns.component("test_component".to_string()).unwrap();
1421 let endpoint = component.endpoint("test_endpoint".to_string());
1422
1423 let client = Client::with_reconcile_interval(endpoint.clone(), TEST_RECONCILE_INTERVAL)
1424 .await
1425 .unwrap();
1426 endpoint.register_endpoint_instance().await.unwrap();
1427 let instances = client.wait_for_instances().await.unwrap();
1428 let worker_id = instances[0].id();
1429
1430 client.set_overloaded_instances(&[worker_id]);
1431 assert_eq!(client.routing_instance_counts().overloaded, 1);
1432 assert!(client.instance_ids_free().is_empty());
1433
1434 endpoint.unregister_endpoint_instance().await.unwrap();
1435 for _ in 0..10 {
1436 if client.routing_instance_counts().overloaded == 0 {
1437 break;
1438 }
1439 tokio::time::sleep(TEST_RECONCILE_INTERVAL).await;
1440 }
1441
1442 assert_eq!(
1443 client.routing_instance_counts().overloaded,
1444 0,
1445 "removed discovered workers should not remain in overloaded state"
1446 );
1447
1448 rt.shutdown();
1449 }
1450
1451 #[tokio::test]
1452 async fn test_instance_ids_free_excludes_overloaded_new_instances() {
1453 const TEST_RECONCILE_INTERVAL: Duration = Duration::from_millis(50);
1454
1455 let rt = Runtime::from_current().unwrap();
1456 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1457 .await
1458 .unwrap();
1459 let worker_id = drt.connection_id();
1460 let ns = drt
1461 .namespace("test_new_overloaded_reconciliation".to_string())
1462 .unwrap();
1463 let component = ns.component("test_component".to_string()).unwrap();
1464 let endpoint = component.endpoint("test_endpoint".to_string());
1465
1466 let client = Client::with_reconcile_interval(endpoint.clone(), TEST_RECONCILE_INTERVAL)
1467 .await
1468 .unwrap();
1469 client.set_overloaded_instances(&[worker_id]);
1470
1471 endpoint.register_endpoint_instance().await.unwrap();
1472 let instances = client.wait_for_instances().await.unwrap();
1473 assert_eq!(instances[0].id(), worker_id);
1474 assert!(
1475 client.instance_ids_free().is_empty(),
1476 "newly discovered overloaded worker should not be free"
1477 );
1478
1479 tokio::time::sleep(TEST_RECONCILE_INTERVAL + Duration::from_millis(50)).await;
1480
1481 assert!(
1482 client.instance_ids_free().is_empty(),
1483 "discovery reconciliation should not affect recomputed free workers"
1484 );
1485
1486 rt.shutdown();
1487 }
1488
1489 #[tokio::test]
1490 async fn test_discovery_add_updates_free_without_overloaded_publish() {
1491 const TEST_RECONCILE_INTERVAL: Duration = Duration::from_millis(50);
1492
1493 let rt = Runtime::from_current().unwrap();
1494 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1495 .await
1496 .unwrap();
1497 let ns = drt
1498 .namespace("test_free_updates_on_discovery_add".to_string())
1499 .unwrap();
1500 let component = ns.component("test_component".to_string()).unwrap();
1501 let endpoint = component.endpoint("test_endpoint".to_string());
1502
1503 let client = Client::with_reconcile_interval(endpoint.clone(), TEST_RECONCILE_INTERVAL)
1504 .await
1505 .unwrap();
1506 endpoint.register_endpoint_instance().await.unwrap();
1507 let instances = client.wait_for_instances().await.unwrap();
1508 let worker_id = instances[0].id();
1509
1510 for _ in 0..10 {
1511 if client.instance_ids_free().contains(&worker_id) {
1512 break;
1513 }
1514 tokio::time::sleep(TEST_RECONCILE_INTERVAL).await;
1515 }
1516
1517 assert_eq!(
1518 client.instance_ids_free(),
1519 vec![worker_id],
1520 "newly discovered non-overloaded workers should appear free without an overload update"
1521 );
1522
1523 rt.shutdown();
1524 }
1525
1526 #[tokio::test]
1528 async fn test_instance_avail_watcher() {
1529 let rt = Runtime::from_current().unwrap();
1530 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1532 .await
1533 .unwrap();
1534 let ns = drt.namespace("test_watcher".to_string()).unwrap();
1535 let component = ns.component("test_component".to_string()).unwrap();
1536 let endpoint = component.endpoint("test_endpoint".to_string());
1537
1538 let client = endpoint.client().await.unwrap();
1539 let watcher = client.instance_avail_watcher();
1540
1541 client.override_instance_avail(vec![1, 2, 3]);
1543
1544 client.report_instance_down(2);
1546
1547 let current = watcher.borrow().clone();
1550 assert_eq!(current, vec![1, 3]);
1551
1552 rt.shutdown();
1553 }
1554
1555 #[tokio::test]
1569 async fn monitor_instance_source_exits_on_its_own_cancellation_token() {
1570 let rt = Runtime::from_current().unwrap();
1571 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1572 .await
1573 .unwrap();
1574 let ns = drt
1575 .namespace("test_monitor_instance_source_cancellation".to_string())
1576 .unwrap();
1577 let component = ns.component("test_component".to_string()).unwrap();
1578 let endpoint = component.endpoint("test_endpoint".to_string());
1579
1580 let cancel_token = tokio_util::sync::CancellationToken::new();
1581 let client = Client::with_cancellation(endpoint.clone(), cancel_token.clone())
1582 .await
1583 .unwrap();
1584
1585 tokio::time::sleep(Duration::from_millis(200)).await;
1588 assert!(
1589 Arc::strong_count(&client.routing_instances) > 1,
1590 "monitor task must be running (and holding its capture) before cancellation"
1591 );
1592
1593 cancel_token.cancel();
1594 tokio::time::timeout(Duration::from_secs(5), async {
1595 while Arc::strong_count(&client.routing_instances) > 1 {
1596 tokio::time::sleep(Duration::from_millis(10)).await;
1597 }
1598 })
1599 .await
1600 .expect("monitor_instance_source task must exit when its cancel_token cancels");
1601
1602 rt.shutdown();
1603 }
1604
1605 #[tokio::test]
1606 async fn admitted_client_never_routes_unadmitted_endpoint_instances() {
1607 let rt = Runtime::from_current().unwrap();
1608 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1609 .await
1610 .unwrap();
1611 let ns = drt.namespace("test_admitted_client".to_string()).unwrap();
1612 let component = ns.component("test_component".to_string()).unwrap();
1613 let endpoint = component.endpoint("test_endpoint".to_string());
1614 let endpoint_client = endpoint.client().await.unwrap();
1615 endpoint.register_endpoint_instance().await.unwrap();
1616 let worker_id = endpoint_client.wait_for_instances().await.unwrap()[0].id();
1617
1618 let (admission_tx, admission_rx) = tokio::sync::watch::channel(Vec::new());
1619 let admitted_client = endpoint_client.with_admitted_instances(admission_rx);
1620 let mut admitted = admitted_client.instance_avail_watcher();
1621 assert!(admitted.borrow().is_empty());
1622
1623 admission_tx.send_replace(vec![worker_id]);
1624 tokio::time::timeout(Duration::from_secs(1), admitted.changed())
1625 .await
1626 .unwrap()
1627 .unwrap();
1628 assert_eq!(admitted.borrow_and_update().clone(), vec![worker_id]);
1629
1630 admission_tx.send_replace(Vec::new());
1631 tokio::time::timeout(Duration::from_secs(1), admitted.changed())
1632 .await
1633 .unwrap()
1634 .unwrap();
1635 assert!(admitted.borrow_and_update().is_empty());
1636
1637 rt.shutdown();
1638 }
1639
1640 #[tokio::test]
1642 async fn test_concurrent_select_and_increment() {
1643 let state = Arc::new(RoutingOccupancyState::default());
1644 let instance_ids: Vec<u64> = vec![100, 200, 300];
1645 let num_requests = 90;
1646
1647 let mut handles = Vec::new();
1648 for _ in 0..num_requests {
1649 let state = state.clone();
1650 let ids = instance_ids.clone();
1651 handles.push(tokio::spawn(async move {
1652 state.select_exact_min_and_increment(&ids).await
1653 }));
1654 }
1655
1656 for handle in handles {
1657 handle.await.unwrap();
1658 }
1659
1660 assert_eq!(state.load(100), 30);
1661 assert_eq!(state.load(200), 30);
1662 assert_eq!(state.load(300), 30);
1663 }
1664
1665 #[tokio::test]
1666 async fn test_select_exact_min_and_increment_randomizes_ties() {
1667 let mut selected = [false; 3];
1668
1669 for _ in 0..120 {
1670 let state = RoutingOccupancyState::default();
1671 let picked = state
1672 .select_exact_min_and_increment(&[10, 20, 30])
1673 .await
1674 .unwrap();
1675 match picked {
1676 10 => selected[0] = true,
1677 20 => selected[1] = true,
1678 30 => selected[2] = true,
1679 _ => panic!("unexpected worker id: {picked}"),
1680 }
1681 }
1682
1683 let selected_count = selected.into_iter().filter(|seen| *seen).count();
1684 assert!(
1685 selected_count > 1,
1686 "tie-breaking should not always select the first minimum-load worker"
1687 );
1688 }
1689
1690 #[tokio::test]
1691 async fn test_connection_counts() {
1692 let rt = Runtime::from_current().unwrap();
1693 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1694 .await
1695 .unwrap();
1696 let ns = drt.namespace("test_ll_counts".to_string()).unwrap();
1697 let component = ns.component("test_component".to_string()).unwrap();
1698 let endpoint = component.endpoint("test_endpoint".to_string());
1699
1700 let state1 = get_or_create_routing_occupancy_state(&endpoint).await;
1701 let state2 = get_or_create_routing_occupancy_state(&endpoint).await;
1702
1703 let picked1 = state1
1704 .select_exact_min_and_increment(&[10, 20, 30])
1705 .await
1706 .unwrap();
1707 assert_eq!(state1.load(picked1), 1);
1708
1709 let picked2 = state1
1710 .select_exact_min_and_increment(&[10, 20, 30])
1711 .await
1712 .unwrap();
1713 assert_ne!(picked1, picked2);
1714
1715 assert_eq!(state2.load(10), state1.load(10));
1717 assert_eq!(state2.load(20), state1.load(20));
1718 assert_eq!(state2.load(30), state1.load(30));
1719
1720 state2.decrement(picked1);
1721 assert_eq!(state1.load(picked1), if picked1 == picked2 { 1 } else { 0 });
1722
1723 rt.shutdown();
1724 }
1725
1726 #[tokio::test]
1727 async fn test_least_loaded_state_retain_preserves_live_counts() {
1728 let state = RoutingOccupancyState::default();
1729
1730 state.select_exact_min_and_increment(&[1, 2, 3]).await;
1732 state.select_exact_min_and_increment(&[1, 2, 3]).await;
1733 state.select_exact_min_and_increment(&[1, 2, 3]).await;
1734 assert_eq!(state.load(1), 1);
1736 assert_eq!(state.load(2), 1);
1737 assert_eq!(state.load(3), 1);
1738
1739 state.retain(&[1, 3]);
1741
1742 assert_eq!(state.load(1), 1);
1743 assert_eq!(state.load(2), 1);
1744 assert_eq!(state.load(3), 1);
1745 }
1746
1747 #[tokio::test]
1748 async fn test_monitor_instance_source_defers_removed_worker_cleanup() {
1749 const TEST_RECONCILE_INTERVAL: Duration = Duration::from_millis(50);
1750
1751 let rt = Runtime::from_current().unwrap();
1752 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1753 .await
1754 .unwrap();
1755 let ns = drt.namespace("test_occupancy_cleanup".to_string()).unwrap();
1756 let component = ns.component("test_component".to_string()).unwrap();
1757 let endpoint = component.endpoint("test_endpoint".to_string());
1758
1759 let client = Client::with_reconcile_interval(endpoint.clone(), TEST_RECONCILE_INTERVAL)
1760 .await
1761 .unwrap();
1762 endpoint.register_endpoint_instance().await.unwrap();
1763 client.wait_for_instances().await.unwrap();
1764
1765 let worker_id = client.instance_ids_avail()[0];
1766 let state = get_or_create_routing_occupancy_state(&endpoint).await;
1767 state.increment(worker_id);
1768 assert_eq!(state.load(worker_id), 1);
1769
1770 endpoint.unregister_endpoint_instance().await.unwrap();
1771
1772 for _ in 0..10 {
1773 if !client.instance_ids().contains(&worker_id) {
1774 break;
1775 }
1776 tokio::time::sleep(TEST_RECONCILE_INTERVAL).await;
1777 }
1778
1779 assert_eq!(
1780 state.load(worker_id),
1781 1,
1782 "discovery absence must retain live accounting"
1783 );
1784 state.decrement(worker_id);
1785 assert_eq!(state.load(worker_id), 0);
1786
1787 rt.shutdown();
1788 }
1789}