1use std::sync::atomic::{AtomicU64, Ordering};
5use std::{
6 collections::{HashMap, HashSet},
7 sync::{Arc, Mutex as StdMutex},
8 time::Duration,
9};
10
11use anyhow::Result;
12use arc_swap::ArcSwap;
13use dashmap::DashMap;
14use futures::StreamExt;
15use rand::Rng;
16
17use crate::component::{Endpoint, Instance};
18use crate::discovery::{DiscoveryEvent, DiscoveryInstance, DiscoveryInstanceId};
19use crate::traits::DistributedRuntimeProvider;
20
21#[derive(Debug, Default)]
23pub(crate) struct RoutingOccupancyState {
24 counts: DashMap<u64, AtomicU64>,
25 exact_selection_lock: tokio::sync::Mutex<()>,
26}
27
28impl RoutingOccupancyState {
29 pub(crate) fn increment(&self, instance_id: u64) {
30 self.counts
31 .entry(instance_id)
32 .or_insert_with(|| AtomicU64::new(0))
33 .fetch_add(1, Ordering::Relaxed);
34 }
35
36 pub(crate) async fn select_exact_min(&self, instance_ids: &[u64]) -> Option<u64> {
37 instance_ids
38 .iter()
39 .min_by_key(|&&id| self.load(id))
40 .copied()
41 }
42
43 pub(crate) async fn select_exact_min_and_increment(&self, instance_ids: &[u64]) -> Option<u64> {
44 let _guard = self.exact_selection_lock.lock().await;
45
46 let mut min_load = u64::MAX;
47 let mut selected = None;
48 let mut tie_count = 0usize;
49 let mut rng = rand::rng();
50 for &id in instance_ids {
51 let load = self.load(id);
52 if load < min_load {
53 min_load = load;
54 selected = Some(id);
55 tie_count = 1;
56 continue;
57 }
58
59 if load == min_load {
60 tie_count += 1;
61 if rng.random_range(0..tie_count) == 0 {
63 selected = Some(id);
64 }
65 }
66 }
67
68 let id = selected?;
69 self.increment(id);
70 Some(id)
71 }
72
73 pub(crate) fn peek_min(&self, instance_ids: &[u64]) -> Option<u64> {
77 let mut min_load = u64::MAX;
78 let mut selected = None;
79 let mut tie_count = 0usize;
80 let mut rng = rand::rng();
81 for &id in instance_ids {
82 let load = self.load(id);
83 if load < min_load {
84 min_load = load;
85 selected = Some(id);
86 tie_count = 1;
87 continue;
88 }
89
90 if load == min_load {
91 tie_count += 1;
92 if rng.random_range(0..tie_count) == 0 {
94 selected = Some(id);
95 }
96 }
97 }
98
99 selected
100 }
101
102 pub(crate) fn decrement(&self, instance_id: u64) {
103 if let Some(count) = self.counts.get(&instance_id) {
104 let _ = count.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
105 Some(current.saturating_sub(1))
106 });
107 }
108 }
109
110 pub(crate) fn load(&self, instance_id: u64) -> u64 {
111 self.counts
112 .get(&instance_id)
113 .map(|c| c.load(Ordering::Relaxed))
114 .unwrap_or(0)
115 }
116
117 pub(crate) fn retain(&self, instance_ids: &[u64]) {
118 let live: HashSet<u64> = instance_ids.iter().copied().collect();
119 self.counts.retain(|id, _| live.contains(id));
120 }
121}
122
123pub(crate) async fn get_or_create_routing_occupancy_state(
125 endpoint: &Endpoint,
126) -> Arc<RoutingOccupancyState> {
127 let drt = endpoint.drt();
128 let registry = drt.routing_occupancy_states();
129 let mut registry = registry.lock().await;
130
131 if let Some(weak) = registry.get(endpoint) {
132 if let Some(state) = weak.upgrade() {
133 return state;
134 } else {
135 registry.remove(endpoint);
136 }
137 }
138
139 let state = Arc::new(RoutingOccupancyState::default());
140 registry.insert(endpoint.clone(), Arc::downgrade(&state));
141 state
142}
143
144const DEFAULT_RECONCILE_INTERVAL: Duration = Duration::from_secs(5);
146
147#[derive(Debug)]
155pub(crate) struct EndpointDiscoverySource {
156 instance_source: tokio::sync::watch::Receiver<Vec<Instance>>,
157 event_subscribers: StdMutex<Vec<tokio::sync::mpsc::UnboundedSender<DiscoveryEvent>>>,
158}
159
160impl EndpointDiscoverySource {
161 fn new(instance_source: tokio::sync::watch::Receiver<Vec<Instance>>) -> Self {
162 Self {
163 instance_source,
164 event_subscribers: StdMutex::new(Vec::new()),
165 }
166 }
167
168 fn instance_receiver(&self) -> tokio::sync::watch::Receiver<Vec<Instance>> {
169 self.instance_source.clone()
170 }
171
172 fn subscribe_events(&self) -> tokio::sync::mpsc::UnboundedReceiver<DiscoveryEvent> {
173 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
174 self.event_subscribers.lock().unwrap().push(tx);
175 rx
176 }
177
178 fn broadcast_event(&self, event: &DiscoveryEvent) {
179 let subscribers = &mut *self.event_subscribers.lock().unwrap();
180 subscribers.retain(|tx| tx.send(event.clone()).is_ok());
181 }
182}
183
184#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
185pub struct RoutingInstanceCounts {
186 pub discovered: usize,
187 pub routable: usize,
188 pub overloaded: usize,
189 pub free: usize,
191}
192
193#[derive(Clone, Debug)]
194pub(crate) struct RoutingInstances {
195 discovered_ids: Vec<u64>,
196 routable_ids: Vec<u64>,
197 overloaded_ids: HashSet<u64>,
198 free_ids: Vec<u64>,
199}
200
201impl RoutingInstances {
202 fn new(discovered_ids: Vec<u64>) -> Self {
203 Self::from_parts(discovered_ids.clone(), discovered_ids, HashSet::new())
204 }
205
206 fn from_parts(
207 discovered_ids: Vec<u64>,
208 routable_ids: Vec<u64>,
209 overloaded_ids: HashSet<u64>,
210 ) -> Self {
211 let free_ids = Self::derive_free_ids(&routable_ids, &overloaded_ids);
212 Self {
213 discovered_ids,
214 routable_ids,
215 overloaded_ids,
216 free_ids,
217 }
218 }
219
220 pub(crate) fn discovered_ids(&self) -> &[u64] {
221 &self.discovered_ids
222 }
223
224 pub(crate) fn routable_ids(&self) -> &[u64] {
225 &self.routable_ids
226 }
227
228 pub(crate) fn free_ids(&self) -> &[u64] {
229 &self.free_ids
230 }
231
232 pub(crate) fn counts(&self) -> RoutingInstanceCounts {
233 RoutingInstanceCounts {
234 discovered: self.discovered_ids.len(),
235 routable: self.routable_ids.len(),
236 overloaded: self.overloaded_ids.len(),
237 free: self.free_ids.len(),
238 }
239 }
240
241 pub(crate) fn is_overloaded(&self, instance_id: u64) -> bool {
242 self.overloaded_ids.contains(&instance_id)
243 }
244
245 fn overloaded_ids(&self) -> Option<HashSet<u64>> {
246 if self.overloaded_ids.is_empty() {
247 return None;
248 }
249
250 Some(self.overloaded_ids.clone())
251 }
252
253 fn reconcile_discovered(&self, discovered_ids: Vec<u64>) -> Self {
254 let old_discovered_ids = self.discovered_ids.iter().copied().collect::<HashSet<_>>();
255 let new_discovered_ids = discovered_ids.iter().copied().collect::<HashSet<_>>();
256 let mut overloaded_ids = self.overloaded_ids.clone();
257 overloaded_ids
258 .retain(|id| !old_discovered_ids.contains(id) || new_discovered_ids.contains(id));
259
260 Self::from_parts(discovered_ids.clone(), discovered_ids, overloaded_ids)
261 }
262
263 fn report_instance_down(&self, instance_id: u64) -> Self {
264 let routable_ids: Vec<u64> = self
265 .routable_ids
266 .iter()
267 .copied()
268 .filter(|id| *id != instance_id)
269 .collect();
270
271 Self::from_parts(
272 self.discovered_ids.clone(),
273 routable_ids,
274 self.overloaded_ids.clone(),
275 )
276 }
277
278 #[cfg(test)]
279 fn override_routable_ids(&self, routable_ids: Vec<u64>) -> Self {
280 Self::from_parts(
283 self.discovered_ids.clone(),
284 routable_ids,
285 self.overloaded_ids.clone(),
286 )
287 }
288
289 fn set_overloaded(&self, overloaded_ids: HashSet<u64>) -> Self {
290 Self::from_parts(
291 self.discovered_ids.clone(),
292 self.routable_ids.clone(),
293 overloaded_ids,
294 )
295 }
296
297 fn mark_overloaded(&self, instance_id: u64) -> Self {
301 let mut overloaded_ids = self.overloaded_ids.clone();
302 overloaded_ids.insert(instance_id);
303 Self::from_parts(
304 self.discovered_ids.clone(),
305 self.routable_ids.clone(),
306 overloaded_ids,
307 )
308 }
309
310 fn clear_overloaded_for_removed(&self, removed_ids: &HashSet<u64>) -> Self {
311 let mut overloaded_ids = self.overloaded_ids.clone();
312 overloaded_ids.retain(|id| !removed_ids.contains(id));
313 Self::from_parts(
314 self.discovered_ids.clone(),
315 self.routable_ids.clone(),
316 overloaded_ids,
317 )
318 }
319
320 fn derive_free_ids(routable_ids: &[u64], overloaded_ids: &HashSet<u64>) -> Vec<u64> {
321 if overloaded_ids.is_empty() {
322 return routable_ids.to_vec();
323 }
324
325 routable_ids
326 .iter()
327 .copied()
328 .filter(|id| !overloaded_ids.contains(id))
329 .collect()
330 }
331}
332
333#[derive(Debug)]
334struct RoutingInstancesState {
335 snapshot: ArcSwap<RoutingInstances>,
336 update_lock: StdMutex<()>,
337 instance_avail_tx: tokio::sync::watch::Sender<Vec<u64>>,
338 instance_avail_rx: tokio::sync::watch::Receiver<Vec<u64>>,
339}
340
341impl RoutingInstancesState {
342 fn new(discovered_ids: Vec<u64>) -> Self {
343 let snapshot = RoutingInstances::new(discovered_ids);
344 let (instance_avail_tx, instance_avail_rx) =
345 tokio::sync::watch::channel(snapshot.routable_ids().to_vec());
346 Self {
347 snapshot: ArcSwap::from_pointee(snapshot),
348 update_lock: StdMutex::new(()),
349 instance_avail_tx,
350 instance_avail_rx,
351 }
352 }
353
354 fn snapshot(&self) -> arc_swap::Guard<Arc<RoutingInstances>> {
355 self.snapshot.load()
356 }
357
358 fn update(
359 &self,
360 update: impl FnOnce(&RoutingInstances) -> RoutingInstances,
361 publish_routable_ids: bool,
362 ) -> Arc<RoutingInstances> {
363 let _guard = self.update_lock.lock().unwrap();
364 let current = self.snapshot.load();
365 let next = Arc::new(update(¤t));
366 self.snapshot.store(next.clone());
367 if publish_routable_ids {
368 self.publish_routable_ids(&next);
369 }
370 next
371 }
372
373 fn publish_routable_ids(&self, routing_instances: &RoutingInstances) {
374 let _ = self
375 .instance_avail_tx
376 .send(routing_instances.routable_ids().to_vec());
377 }
378
379 fn routable_ids(&self) -> Vec<u64> {
380 self.snapshot().routable_ids().to_vec()
381 }
382
383 fn free_ids(&self) -> Vec<u64> {
384 self.snapshot().free_ids.clone()
385 }
386
387 fn counts(&self) -> RoutingInstanceCounts {
388 self.snapshot().counts()
389 }
390
391 fn overloaded_ids(&self) -> Option<HashSet<u64>> {
392 self.snapshot().overloaded_ids()
393 }
394
395 fn instance_avail_watcher(&self) -> tokio::sync::watch::Receiver<Vec<u64>> {
396 self.instance_avail_rx.clone()
397 }
398
399 fn report_instance_down(&self, instance_id: u64) {
400 self.update(|current| current.report_instance_down(instance_id), true);
401 }
402
403 fn set_overloaded_instances(&self, overloaded_instance_ids: &[u64]) -> bool {
404 let overloaded_ids = overloaded_instance_ids
405 .iter()
406 .copied()
407 .collect::<HashSet<_>>();
408 let _guard = self.update_lock.lock().unwrap();
409 let current = self.snapshot.load();
410 if current.overloaded_ids == overloaded_ids {
411 return false;
412 }
413
414 let next = Arc::new(current.set_overloaded(overloaded_ids));
415 self.snapshot.store(next);
416 true
417 }
418
419 fn mark_overloaded_immediate(&self, instance_id: u64) {
420 self.update(
421 move |current| current.mark_overloaded(instance_id),
422 false,
425 );
426 }
427
428 fn clear_overloaded_for_removed(&self, removed_instance_ids: &[u64]) {
429 if removed_instance_ids.is_empty() {
430 return;
431 }
432
433 let removed_ids = removed_instance_ids.iter().copied().collect::<HashSet<_>>();
434 self.update(
435 move |current| current.clear_overloaded_for_removed(&removed_ids),
436 false,
437 );
438 }
439
440 fn reconcile_discovered(&self, discovered_ids: Vec<u64>) -> Arc<RoutingInstances> {
441 self.update(
442 move |current| current.reconcile_discovered(discovered_ids),
443 true,
444 )
445 }
446
447 #[cfg(test)]
448 fn override_routable_ids(&self, ids: Vec<u64>) {
449 self.update(move |current| current.override_routable_ids(ids), true);
450 }
451}
452
453#[derive(Clone, Debug)]
454pub struct Client {
455 pub endpoint: Endpoint,
457 endpoint_discovery_source: Arc<EndpointDiscoverySource>,
459 pub instance_source: Arc<tokio::sync::watch::Receiver<Vec<Instance>>>,
461 routing_instances: Arc<RoutingInstancesState>,
463 reconcile_interval: Duration,
466}
467
468impl Client {
469 pub(crate) async fn new(endpoint: Endpoint) -> Result<Self> {
471 Self::with_reconcile_interval(endpoint, DEFAULT_RECONCILE_INTERVAL).await
472 }
473
474 pub(crate) async fn with_reconcile_interval(
478 endpoint: Endpoint,
479 reconcile_interval: Duration,
480 ) -> Result<Self> {
481 tracing::trace!(
482 "Client::new_dynamic: Creating dynamic client for endpoint: {}",
483 endpoint.id()
484 );
485 let endpoint_discovery_source =
486 Self::get_or_create_dynamic_discovery_source(&endpoint).await?;
487 let instance_source = Arc::new(endpoint_discovery_source.instance_receiver());
488
489 let initial_ids: Vec<u64> = instance_source
494 .borrow()
495 .iter()
496 .map(|instance| instance.id())
497 .collect();
498 let client = Client {
499 endpoint: endpoint.clone(),
500 endpoint_discovery_source,
501 instance_source: instance_source.clone(),
502 routing_instances: Arc::new(RoutingInstancesState::new(initial_ids)),
503 reconcile_interval,
504 };
505 client.monitor_instance_source();
506 Ok(client)
507 }
508
509 pub fn instances(&self) -> Vec<Instance> {
511 self.instance_source.borrow().clone()
512 }
513
514 pub fn instance_ids(&self) -> Vec<u64> {
515 self.instances().into_iter().map(|ep| ep.id()).collect()
516 }
517
518 pub fn instance_ids_avail(&self) -> Vec<u64> {
519 self.routing_instances.routable_ids()
520 }
521
522 pub fn instance_ids_free(&self) -> Vec<u64> {
525 self.routing_instances.free_ids()
526 }
527
528 pub(crate) fn routing_instances(&self) -> arc_swap::Guard<Arc<RoutingInstances>> {
529 self.routing_instances.snapshot()
530 }
531
532 pub fn routing_instance_counts(&self) -> RoutingInstanceCounts {
533 self.routing_instances.counts()
534 }
535
536 pub fn instance_avail_watcher(&self) -> tokio::sync::watch::Receiver<Vec<u64>> {
538 self.routing_instances.instance_avail_watcher()
539 }
540
541 pub(crate) fn subscribe_discovery_events(
546 &self,
547 ) -> tokio::sync::mpsc::UnboundedReceiver<DiscoveryEvent> {
548 self.endpoint_discovery_source.subscribe_events()
549 }
550
551 pub async fn wait_for_instances(&self) -> Result<Vec<Instance>> {
553 tracing::trace!(
554 "wait_for_instances: Starting wait for endpoint: {}",
555 self.endpoint.id()
556 );
557 let mut rx = self.instance_source.as_ref().clone();
558 let mut instances: Vec<Instance>;
560 loop {
561 instances = rx.borrow_and_update().to_vec();
562 if instances.is_empty() {
563 rx.changed().await?;
564 } else {
565 tracing::info!(
566 "wait_for_instances: Found {} instance(s) for endpoint: {}",
567 instances.len(),
568 self.endpoint.id()
569 );
570 break;
571 }
572 }
573 Ok(instances)
574 }
575
576 pub fn report_instance_down(&self, instance_id: u64) {
578 self.routing_instances.report_instance_down(instance_id);
579 tracing::debug!("inhibiting instance {instance_id}");
580 }
581
582 pub fn set_overloaded_instances(&self, overloaded_instance_ids: &[u64]) -> bool {
585 self.routing_instances
586 .set_overloaded_instances(overloaded_instance_ids)
587 }
588
589 pub fn mark_overloaded_immediate(&self, instance_id: u64) {
594 self.routing_instances
595 .mark_overloaded_immediate(instance_id);
596 tracing::debug!(
597 instance_id,
598 "marking instance overloaded (backpressure); next metric event will re-evaluate"
599 );
600 }
601
602 pub fn clear_overloaded_instances_for_removed(&self, removed_instance_ids: &[u64]) {
603 self.routing_instances
604 .clear_overloaded_for_removed(removed_instance_ids);
605 }
606
607 pub fn overloaded_instance_ids(&self) -> Option<HashSet<u64>> {
608 self.routing_instances.overloaded_ids()
609 }
610
611 fn monitor_instance_source(&self) {
618 let reconcile_interval = self.reconcile_interval;
619 let cancel_token = self.endpoint.drt().primary_token();
620 let client = self.clone();
621 let endpoint_id = self.endpoint.id();
622 tokio::task::spawn(async move {
623 let mut rx = client.instance_source.as_ref().clone();
624 while !cancel_token.is_cancelled() {
625 let instance_ids: Vec<u64> = rx
626 .borrow_and_update()
627 .iter()
628 .map(|instance| instance.id())
629 .collect();
630
631 let routing_instances = client.reconcile_discovered_instances(instance_ids);
632
633 let registry = client.endpoint.drt().routing_occupancy_states();
635 if let Ok(registry) = registry.try_lock()
636 && let Some(weak) = registry.get(&client.endpoint)
637 && let Some(state) = weak.upgrade()
638 {
639 state.retain(routing_instances.discovered_ids());
640 }
641
642 tokio::select! {
643 result = rx.changed() => {
644 if let Err(err) = result {
645 tracing::error!(
646 "monitor_instance_source: The Sender is dropped: {err}, endpoint={endpoint_id}",
647 );
648 cancel_token.cancel();
649 }
650 }
651 _ = tokio::time::sleep(reconcile_interval) => {
652 tracing::trace!(
653 "monitor_instance_source: periodic reconciliation for endpoint={endpoint_id}",
654 );
655 }
656 }
657 }
658 });
659 }
660
661 #[cfg(test)]
664 pub(crate) fn override_instance_avail(&self, ids: Vec<u64>) {
665 self.routing_instances.override_routable_ids(ids);
666 }
667
668 fn reconcile_discovered_instances(&self, discovered_ids: Vec<u64>) -> Arc<RoutingInstances> {
669 self.routing_instances.reconcile_discovered(discovered_ids)
670 }
671
672 async fn get_or_create_dynamic_discovery_source(
673 endpoint: &Endpoint,
674 ) -> Result<Arc<EndpointDiscoverySource>> {
675 let drt = endpoint.drt();
676 let sources = drt.endpoint_discovery_sources();
677 let mut sources = sources.lock().await;
678
679 if let Some(source) = sources.get(endpoint) {
680 if let Some(source) = source.upgrade() {
681 return Ok(source);
682 } else {
683 sources.remove(endpoint);
684 }
685 }
686
687 let discovery = drt.discovery();
688 let discovery_query = crate::discovery::DiscoveryQuery::Endpoint {
689 namespace: endpoint.component.namespace.name.clone(),
690 component: endpoint.component.name.clone(),
691 endpoint: endpoint.name.clone(),
692 };
693
694 let mut discovery_stream = discovery
695 .list_and_watch(discovery_query.clone(), None)
696 .await?;
697 let (watch_tx, watch_rx) = tokio::sync::watch::channel(vec![]);
698 let discovery_source = Arc::new(EndpointDiscoverySource::new(watch_rx));
699
700 let secondary = endpoint.component.drt.runtime().secondary().clone();
701 let discovery_source_task = discovery_source.clone();
702
703 secondary.spawn(async move {
704 tracing::trace!("endpoint_watcher: Starting for discovery query: {:?}", discovery_query);
705 let mut map: HashMap<u64, Instance> = HashMap::new();
706
707 loop {
708 let discovery_event = tokio::select! {
709 _ = watch_tx.closed() => {
710 break;
711 }
712 discovery_event = discovery_stream.next() => {
713 match discovery_event {
714 Some(Ok(event)) => {
715 event
716 },
717 Some(Err(e)) => {
718 tracing::error!("endpoint_watcher: discovery stream error: {}; shutting down for discovery query: {:?}", e, discovery_query);
719 break;
720 }
721 None => {
722 break;
723 }
724 }
725 }
726 };
727
728 discovery_source_task.broadcast_event(&discovery_event);
729
730 match discovery_event {
731 DiscoveryEvent::Added(DiscoveryInstance::Endpoint(instance)) => {
732 map.insert(instance.instance_id, instance);
733 }
734 DiscoveryEvent::Added(_) => {}
735 DiscoveryEvent::Removed(id) => {
736 if let DiscoveryInstanceId::Endpoint(endpoint_id) = id {
737 map.remove(&endpoint_id.instance_id);
738 }
739 }
740 }
741
742 let instances: Vec<Instance> = map.values().cloned().collect();
743 if watch_tx.send(instances).is_err() {
744 break;
745 }
746 }
747 let _ = watch_tx.send(vec![]);
748 });
749
750 sources.insert(endpoint.clone(), Arc::downgrade(&discovery_source));
751 Ok(discovery_source)
752 }
753}
754
755#[cfg(test)]
756mod tests {
757 use super::*;
758 use crate::{DistributedRuntime, Runtime, distributed::DistributedConfig};
759
760 #[tokio::test]
763 async fn test_instance_reconciliation() {
764 const TEST_RECONCILE_INTERVAL: Duration = Duration::from_millis(100);
765
766 let rt = Runtime::from_current().unwrap();
767 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
769 .await
770 .unwrap();
771 let ns = drt.namespace("test_reconciliation".to_string()).unwrap();
772 let component = ns.component("test_component".to_string()).unwrap();
773 let endpoint = component.endpoint("test_endpoint".to_string());
774
775 let client = Client::with_reconcile_interval(endpoint, TEST_RECONCILE_INTERVAL)
777 .await
778 .unwrap();
779
780 assert!(client.instance_ids_avail().is_empty());
782
783 client.override_instance_avail(vec![1, 2, 3]);
786
787 assert_eq!(client.instance_ids_avail(), vec![1u64, 2, 3]);
788
789 client.report_instance_down(2);
791 assert_eq!(client.instance_ids_avail(), vec![1u64, 3]);
792
793 tokio::time::sleep(TEST_RECONCILE_INTERVAL + Duration::from_millis(50)).await;
797
798 assert!(
800 client.instance_ids_avail().is_empty(),
801 "After reconciliation, instance_avail should match instance_source"
802 );
803
804 rt.shutdown();
805 }
806
807 #[tokio::test]
809 async fn test_report_instance_down() {
810 let rt = Runtime::from_current().unwrap();
811 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
813 .await
814 .unwrap();
815 let ns = drt.namespace("test_report_down".to_string()).unwrap();
816 let component = ns.component("test_component".to_string()).unwrap();
817 let endpoint = component.endpoint("test_endpoint".to_string());
818
819 let client = endpoint.client().await.unwrap();
820
821 client.override_instance_avail(vec![1, 2, 3]);
823 assert_eq!(client.instance_ids_avail(), vec![1u64, 2, 3]);
824
825 client.report_instance_down(2);
827
828 let avail = client.instance_ids_avail();
830 assert!(avail.contains(&1), "Instance 1 should still be available");
831 assert!(
832 !avail.contains(&2),
833 "Instance 2 should be removed after report_instance_down"
834 );
835 assert!(avail.contains(&3), "Instance 3 should still be available");
836
837 rt.shutdown();
838 }
839
840 #[tokio::test]
841 async fn test_overloaded_instance_ids_returns_none_when_empty() {
842 let rt = Runtime::from_current().unwrap();
843 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
844 .await
845 .unwrap();
846 let ns = drt.namespace("test_overloaded_ids".to_string()).unwrap();
847 let component = ns.component("test_component".to_string()).unwrap();
848 let endpoint = component.endpoint("test_endpoint".to_string());
849 let client = endpoint.client().await.unwrap();
850
851 assert_eq!(client.overloaded_instance_ids(), None);
852
853 assert!(client.set_overloaded_instances(&[7]));
854 assert_eq!(client.overloaded_instance_ids(), Some(HashSet::from([7])));
855 assert!(!client.set_overloaded_instances(&[7]));
856
857 assert!(client.set_overloaded_instances(&[]));
858 assert_eq!(client.overloaded_instance_ids(), None);
859 assert!(!client.set_overloaded_instances(&[]));
860
861 rt.shutdown();
862 }
863
864 #[tokio::test]
865 async fn test_instance_reconciliation_preserves_overloaded_existing_instances() {
866 const TEST_RECONCILE_INTERVAL: Duration = Duration::from_millis(50);
867
868 let rt = Runtime::from_current().unwrap();
869 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
870 .await
871 .unwrap();
872 let ns = drt
873 .namespace("test_overloaded_reconciliation".to_string())
874 .unwrap();
875 let component = ns.component("test_component".to_string()).unwrap();
876 let endpoint = component.endpoint("test_endpoint".to_string());
877
878 let client = Client::with_reconcile_interval(endpoint.clone(), TEST_RECONCILE_INTERVAL)
879 .await
880 .unwrap();
881 endpoint.register_endpoint_instance().await.unwrap();
882 let instances = client.wait_for_instances().await.unwrap();
883 let worker_id = instances[0].id();
884
885 for _ in 0..10 {
886 if client.instance_ids_free().contains(&worker_id) {
887 break;
888 }
889 tokio::time::sleep(TEST_RECONCILE_INTERVAL).await;
890 }
891 assert!(
892 client.instance_ids_free().contains(&worker_id),
893 "worker should be free after initial discovery reconciliation"
894 );
895
896 client.set_overloaded_instances(&[worker_id]);
897 assert!(
898 client.instance_ids_free().is_empty(),
899 "worker should be overloaded before periodic reconciliation"
900 );
901
902 tokio::time::sleep(TEST_RECONCILE_INTERVAL + Duration::from_millis(50)).await;
903
904 assert!(
905 client.instance_ids_free().is_empty(),
906 "periodic reconciliation should not mark an existing overloaded worker free"
907 );
908
909 rt.shutdown();
910 }
911
912 #[tokio::test]
913 async fn test_report_instance_down_preserves_overloaded_state() {
914 const TEST_RECONCILE_INTERVAL: Duration = Duration::from_millis(50);
915
916 let rt = Runtime::from_current().unwrap();
917 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
918 .await
919 .unwrap();
920 let ns = drt
921 .namespace("test_report_down_preserves_overloaded".to_string())
922 .unwrap();
923 let component = ns.component("test_component".to_string()).unwrap();
924 let endpoint = component.endpoint("test_endpoint".to_string());
925
926 let client = Client::with_reconcile_interval(endpoint.clone(), TEST_RECONCILE_INTERVAL)
927 .await
928 .unwrap();
929 endpoint.register_endpoint_instance().await.unwrap();
930 let instances = client.wait_for_instances().await.unwrap();
931 let worker_id = instances[0].id();
932
933 for _ in 0..10 {
934 if client.instance_ids_avail().contains(&worker_id) {
935 break;
936 }
937 tokio::time::sleep(TEST_RECONCILE_INTERVAL).await;
938 }
939
940 client.set_overloaded_instances(&[worker_id]);
941 client.report_instance_down(worker_id);
942
943 assert!(
944 !client.instance_ids_avail().contains(&worker_id),
945 "reported-down worker should leave routable availability"
946 );
947 assert_eq!(
948 client.routing_instance_counts().overloaded,
949 1,
950 "reported-down worker should remain overloaded while still discovered"
951 );
952 assert!(
953 client.instance_ids_free().is_empty(),
954 "reported-down overloaded worker should not become free"
955 );
956
957 endpoint.unregister_endpoint_instance().await.unwrap();
958 for _ in 0..10 {
959 if client.routing_instance_counts().overloaded == 0 {
960 break;
961 }
962 tokio::time::sleep(TEST_RECONCILE_INTERVAL).await;
963 }
964
965 assert_eq!(
966 client.routing_instance_counts().overloaded,
967 0,
968 "stable discovery removal should clear overloaded state"
969 );
970
971 rt.shutdown();
972 }
973
974 #[tokio::test]
975 async fn test_instance_reconciliation_prunes_removed_overloaded_instances() {
976 const TEST_RECONCILE_INTERVAL: Duration = Duration::from_millis(50);
977
978 let rt = Runtime::from_current().unwrap();
979 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
980 .await
981 .unwrap();
982 let ns = drt
983 .namespace("test_removed_overloaded_cleanup".to_string())
984 .unwrap();
985 let component = ns.component("test_component".to_string()).unwrap();
986 let endpoint = component.endpoint("test_endpoint".to_string());
987
988 let client = Client::with_reconcile_interval(endpoint.clone(), TEST_RECONCILE_INTERVAL)
989 .await
990 .unwrap();
991 endpoint.register_endpoint_instance().await.unwrap();
992 let instances = client.wait_for_instances().await.unwrap();
993 let worker_id = instances[0].id();
994
995 client.set_overloaded_instances(&[worker_id]);
996 assert_eq!(client.routing_instance_counts().overloaded, 1);
997 assert!(client.instance_ids_free().is_empty());
998
999 endpoint.unregister_endpoint_instance().await.unwrap();
1000 for _ in 0..10 {
1001 if client.routing_instance_counts().overloaded == 0 {
1002 break;
1003 }
1004 tokio::time::sleep(TEST_RECONCILE_INTERVAL).await;
1005 }
1006
1007 assert_eq!(
1008 client.routing_instance_counts().overloaded,
1009 0,
1010 "removed discovered workers should not remain in overloaded state"
1011 );
1012
1013 rt.shutdown();
1014 }
1015
1016 #[tokio::test]
1017 async fn test_instance_ids_free_excludes_overloaded_new_instances() {
1018 const TEST_RECONCILE_INTERVAL: Duration = Duration::from_millis(50);
1019
1020 let rt = Runtime::from_current().unwrap();
1021 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1022 .await
1023 .unwrap();
1024 let worker_id = drt.connection_id();
1025 let ns = drt
1026 .namespace("test_new_overloaded_reconciliation".to_string())
1027 .unwrap();
1028 let component = ns.component("test_component".to_string()).unwrap();
1029 let endpoint = component.endpoint("test_endpoint".to_string());
1030
1031 let client = Client::with_reconcile_interval(endpoint.clone(), TEST_RECONCILE_INTERVAL)
1032 .await
1033 .unwrap();
1034 client.set_overloaded_instances(&[worker_id]);
1035
1036 endpoint.register_endpoint_instance().await.unwrap();
1037 let instances = client.wait_for_instances().await.unwrap();
1038 assert_eq!(instances[0].id(), worker_id);
1039 assert!(
1040 client.instance_ids_free().is_empty(),
1041 "newly discovered overloaded worker should not be free"
1042 );
1043
1044 tokio::time::sleep(TEST_RECONCILE_INTERVAL + Duration::from_millis(50)).await;
1045
1046 assert!(
1047 client.instance_ids_free().is_empty(),
1048 "discovery reconciliation should not affect recomputed free workers"
1049 );
1050
1051 rt.shutdown();
1052 }
1053
1054 #[tokio::test]
1055 async fn test_discovery_add_updates_free_without_overloaded_publish() {
1056 const TEST_RECONCILE_INTERVAL: Duration = Duration::from_millis(50);
1057
1058 let rt = Runtime::from_current().unwrap();
1059 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1060 .await
1061 .unwrap();
1062 let ns = drt
1063 .namespace("test_free_updates_on_discovery_add".to_string())
1064 .unwrap();
1065 let component = ns.component("test_component".to_string()).unwrap();
1066 let endpoint = component.endpoint("test_endpoint".to_string());
1067
1068 let client = Client::with_reconcile_interval(endpoint.clone(), TEST_RECONCILE_INTERVAL)
1069 .await
1070 .unwrap();
1071 endpoint.register_endpoint_instance().await.unwrap();
1072 let instances = client.wait_for_instances().await.unwrap();
1073 let worker_id = instances[0].id();
1074
1075 for _ in 0..10 {
1076 if client.instance_ids_free().contains(&worker_id) {
1077 break;
1078 }
1079 tokio::time::sleep(TEST_RECONCILE_INTERVAL).await;
1080 }
1081
1082 assert_eq!(
1083 client.instance_ids_free(),
1084 vec![worker_id],
1085 "newly discovered non-overloaded workers should appear free without an overload update"
1086 );
1087
1088 rt.shutdown();
1089 }
1090
1091 #[tokio::test]
1093 async fn test_instance_avail_watcher() {
1094 let rt = Runtime::from_current().unwrap();
1095 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1097 .await
1098 .unwrap();
1099 let ns = drt.namespace("test_watcher".to_string()).unwrap();
1100 let component = ns.component("test_component".to_string()).unwrap();
1101 let endpoint = component.endpoint("test_endpoint".to_string());
1102
1103 let client = endpoint.client().await.unwrap();
1104 let watcher = client.instance_avail_watcher();
1105
1106 client.override_instance_avail(vec![1, 2, 3]);
1108
1109 client.report_instance_down(2);
1111
1112 let current = watcher.borrow().clone();
1115 assert_eq!(current, vec![1, 3]);
1116
1117 rt.shutdown();
1118 }
1119
1120 #[tokio::test]
1122 async fn test_concurrent_select_and_increment() {
1123 let state = Arc::new(RoutingOccupancyState::default());
1124 let instance_ids: Vec<u64> = vec![100, 200, 300];
1125 let num_requests = 90;
1126
1127 let mut handles = Vec::new();
1128 for _ in 0..num_requests {
1129 let state = state.clone();
1130 let ids = instance_ids.clone();
1131 handles.push(tokio::spawn(async move {
1132 state.select_exact_min_and_increment(&ids).await
1133 }));
1134 }
1135
1136 for handle in handles {
1137 handle.await.unwrap();
1138 }
1139
1140 assert_eq!(state.load(100), 30);
1141 assert_eq!(state.load(200), 30);
1142 assert_eq!(state.load(300), 30);
1143 }
1144
1145 #[tokio::test]
1146 async fn test_select_exact_min_and_increment_randomizes_ties() {
1147 let mut selected = [false; 3];
1148
1149 for _ in 0..120 {
1150 let state = RoutingOccupancyState::default();
1151 let picked = state
1152 .select_exact_min_and_increment(&[10, 20, 30])
1153 .await
1154 .unwrap();
1155 match picked {
1156 10 => selected[0] = true,
1157 20 => selected[1] = true,
1158 30 => selected[2] = true,
1159 _ => panic!("unexpected worker id: {picked}"),
1160 }
1161 }
1162
1163 let selected_count = selected.into_iter().filter(|seen| *seen).count();
1164 assert!(
1165 selected_count > 1,
1166 "tie-breaking should not always select the first minimum-load worker"
1167 );
1168 }
1169
1170 #[tokio::test]
1171 async fn test_connection_counts() {
1172 let rt = Runtime::from_current().unwrap();
1173 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1174 .await
1175 .unwrap();
1176 let ns = drt.namespace("test_ll_counts".to_string()).unwrap();
1177 let component = ns.component("test_component".to_string()).unwrap();
1178 let endpoint = component.endpoint("test_endpoint".to_string());
1179
1180 let state1 = get_or_create_routing_occupancy_state(&endpoint).await;
1181 let state2 = get_or_create_routing_occupancy_state(&endpoint).await;
1182
1183 let picked1 = state1
1184 .select_exact_min_and_increment(&[10, 20, 30])
1185 .await
1186 .unwrap();
1187 assert_eq!(state1.load(picked1), 1);
1188
1189 let picked2 = state1
1190 .select_exact_min_and_increment(&[10, 20, 30])
1191 .await
1192 .unwrap();
1193 assert_ne!(picked1, picked2);
1194
1195 assert_eq!(state2.load(10), state1.load(10));
1197 assert_eq!(state2.load(20), state1.load(20));
1198 assert_eq!(state2.load(30), state1.load(30));
1199
1200 state2.decrement(picked1);
1201 assert_eq!(state1.load(picked1), if picked1 == picked2 { 1 } else { 0 });
1202
1203 rt.shutdown();
1204 }
1205
1206 #[tokio::test]
1207 async fn test_least_loaded_state_retain() {
1208 let state = RoutingOccupancyState::default();
1209
1210 state.select_exact_min_and_increment(&[1, 2, 3]).await;
1212 state.select_exact_min_and_increment(&[1, 2, 3]).await;
1213 state.select_exact_min_and_increment(&[1, 2, 3]).await;
1214 assert_eq!(state.load(1), 1);
1216 assert_eq!(state.load(2), 1);
1217 assert_eq!(state.load(3), 1);
1218
1219 state.retain(&[1, 3]);
1221
1222 assert_eq!(state.load(1), 1);
1223 assert_eq!(state.load(2), 0);
1224 assert_eq!(state.load(3), 1);
1225 }
1226
1227 #[tokio::test]
1228 async fn test_monitor_instance_source_cleans_up_removed_worker_counts() {
1229 const TEST_RECONCILE_INTERVAL: Duration = Duration::from_millis(50);
1230
1231 let rt = Runtime::from_current().unwrap();
1232 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1233 .await
1234 .unwrap();
1235 let ns = drt.namespace("test_occupancy_cleanup".to_string()).unwrap();
1236 let component = ns.component("test_component".to_string()).unwrap();
1237 let endpoint = component.endpoint("test_endpoint".to_string());
1238
1239 let client = Client::with_reconcile_interval(endpoint.clone(), TEST_RECONCILE_INTERVAL)
1240 .await
1241 .unwrap();
1242 endpoint.register_endpoint_instance().await.unwrap();
1243 client.wait_for_instances().await.unwrap();
1244
1245 let worker_id = client.instance_ids_avail()[0];
1246 let state = get_or_create_routing_occupancy_state(&endpoint).await;
1247 state.increment(worker_id);
1248 assert_eq!(state.load(worker_id), 1);
1249
1250 endpoint.unregister_endpoint_instance().await.unwrap();
1251
1252 for _ in 0..10 {
1253 if state.load(worker_id) == 0 {
1254 break;
1255 }
1256 tokio::time::sleep(TEST_RECONCILE_INTERVAL).await;
1257 }
1258
1259 assert_eq!(state.load(worker_id), 0);
1260
1261 rt.shutdown();
1262 }
1263}