1use std::sync::atomic::{AtomicU64, Ordering};
5use std::{
6 collections::{HashMap, HashSet},
7 sync::{Arc, LazyLock, 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::config::environment_names::runtime as env_runtime;
19use crate::discovery::{DiscoveryEvent, DiscoveryInstance, DiscoveryInstanceId};
20use crate::traits::DistributedRuntimeProvider;
21
22#[derive(Debug, Default)]
24pub(crate) struct RoutingOccupancyState {
25 counts: DashMap<u64, AtomicU64>,
26 exact_selection_lock: tokio::sync::Mutex<()>,
27}
28
29impl RoutingOccupancyState {
30 pub(crate) fn increment(&self, instance_id: u64) {
31 self.counts
32 .entry(instance_id)
33 .or_insert_with(|| AtomicU64::new(0))
34 .fetch_add(1, Ordering::Relaxed);
35 }
36
37 pub(crate) async fn select_exact_min(&self, instance_ids: &[u64]) -> Option<u64> {
38 instance_ids
39 .iter()
40 .min_by_key(|&&id| self.load(id))
41 .copied()
42 }
43
44 pub(crate) async fn select_exact_min_and_increment(&self, instance_ids: &[u64]) -> Option<u64> {
45 let _guard = self.exact_selection_lock.lock().await;
46
47 let mut min_load = u64::MAX;
48 let mut selected = None;
49 let mut tie_count = 0usize;
50 let mut rng = rand::rng();
51 for &id in instance_ids {
52 let load = self.load(id);
53 if load < min_load {
54 min_load = load;
55 selected = Some(id);
56 tie_count = 1;
57 continue;
58 }
59
60 if load == min_load {
61 tie_count += 1;
62 if rng.random_range(0..tie_count) == 0 {
64 selected = Some(id);
65 }
66 }
67 }
68
69 let id = selected?;
70 self.increment(id);
71 Some(id)
72 }
73
74 pub(crate) fn peek_min(&self, instance_ids: &[u64]) -> Option<u64> {
78 let mut min_load = u64::MAX;
79 let mut selected = None;
80 let mut tie_count = 0usize;
81 let mut rng = rand::rng();
82 for &id in instance_ids {
83 let load = self.load(id);
84 if load < min_load {
85 min_load = load;
86 selected = Some(id);
87 tie_count = 1;
88 continue;
89 }
90
91 if load == min_load {
92 tie_count += 1;
93 if rng.random_range(0..tie_count) == 0 {
95 selected = Some(id);
96 }
97 }
98 }
99
100 selected
101 }
102
103 pub(crate) fn decrement(&self, instance_id: u64) {
104 if let Some(count) = self.counts.get(&instance_id) {
105 let _ = count.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
106 Some(current.saturating_sub(1))
107 });
108 }
109 }
110
111 pub(crate) fn load(&self, instance_id: u64) -> u64 {
112 self.counts
113 .get(&instance_id)
114 .map(|c| c.load(Ordering::Relaxed))
115 .unwrap_or(0)
116 }
117
118 pub(crate) fn retain(&self, instance_ids: &[u64]) {
119 let live: HashSet<u64> = instance_ids.iter().copied().collect();
120 self.counts.retain(|id, _| live.contains(id));
121 }
122}
123
124pub(crate) async fn get_or_create_routing_occupancy_state(
126 endpoint: &Endpoint,
127) -> Arc<RoutingOccupancyState> {
128 let drt = endpoint.drt();
129 let registry = drt.routing_occupancy_states();
130 let mut registry = registry.lock().await;
131
132 if let Some(weak) = registry.get(endpoint) {
133 if let Some(state) = weak.upgrade() {
134 return state;
135 } else {
136 registry.remove(endpoint);
137 }
138 }
139
140 let state = Arc::new(RoutingOccupancyState::default());
141 registry.insert(endpoint.clone(), Arc::downgrade(&state));
142 state
143}
144
145const DEFAULT_INHIBITED_DURATION_SECS: u64 = 5;
147
148static INHIBITED_DURATION: LazyLock<Duration> =
150 LazyLock::new(|| inhibited_duration_from_env(|name| std::env::var(name).ok()));
151
152fn inhibited_duration_from_env(mut lookup: impl FnMut(&str) -> Option<String>) -> Duration {
153 let seconds = match lookup(env_runtime::DYN_RUNTIME_INHIBITED_DURATION_SECS) {
154 None => DEFAULT_INHIBITED_DURATION_SECS,
155 Some(raw) => match raw.parse::<u64>() {
156 Ok(seconds) => seconds,
157 Err(err) => {
158 tracing::warn!(
159 value = raw,
160 %err,
161 "invalid {}; using the default of {} seconds",
162 env_runtime::DYN_RUNTIME_INHIBITED_DURATION_SECS,
163 DEFAULT_INHIBITED_DURATION_SECS,
164 );
165 DEFAULT_INHIBITED_DURATION_SECS
166 }
167 },
168 };
169 Duration::from_secs(seconds)
170}
171
172#[derive(Debug)]
180pub(crate) struct EndpointDiscoverySource {
181 instance_source: tokio::sync::watch::Receiver<Vec<Instance>>,
182 event_subscribers: StdMutex<Vec<tokio::sync::mpsc::UnboundedSender<DiscoveryEvent>>>,
183}
184
185pub(crate) struct DiscoveryEventReceiver {
186 receiver: tokio::sync::mpsc::UnboundedReceiver<DiscoveryEvent>,
187 _source: Arc<EndpointDiscoverySource>,
188}
189
190impl std::ops::Deref for DiscoveryEventReceiver {
191 type Target = tokio::sync::mpsc::UnboundedReceiver<DiscoveryEvent>;
192
193 fn deref(&self) -> &Self::Target {
194 &self.receiver
195 }
196}
197
198impl std::ops::DerefMut for DiscoveryEventReceiver {
199 fn deref_mut(&mut self) -> &mut Self::Target {
200 &mut self.receiver
201 }
202}
203
204impl EndpointDiscoverySource {
205 fn new(instance_source: tokio::sync::watch::Receiver<Vec<Instance>>) -> Self {
206 Self {
207 instance_source,
208 event_subscribers: StdMutex::new(Vec::new()),
209 }
210 }
211
212 fn instance_receiver(&self) -> tokio::sync::watch::Receiver<Vec<Instance>> {
213 self.instance_source.clone()
214 }
215
216 fn subscribe_events(&self) -> tokio::sync::mpsc::UnboundedReceiver<DiscoveryEvent> {
217 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
218 self.event_subscribers.lock().unwrap().push(tx);
219 rx
220 }
221
222 fn broadcast_event(&self, event: &DiscoveryEvent) {
223 let subscribers = &mut *self.event_subscribers.lock().unwrap();
224 subscribers.retain(|tx| tx.send(event.clone()).is_ok());
225 }
226}
227
228#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
229pub struct RoutingInstanceCounts {
230 pub discovered: usize,
231 pub routable: usize,
232 pub overloaded: usize,
233 pub free: usize,
235}
236
237#[derive(Clone, Debug)]
238pub(crate) struct RoutingInstances {
239 discovered_ids: Vec<u64>,
240 routable_ids: Vec<u64>,
241 overloaded_ids: HashSet<u64>,
242 free_ids: Vec<u64>,
243}
244
245impl RoutingInstances {
246 fn new(discovered_ids: Vec<u64>) -> Self {
247 Self::from_parts(discovered_ids.clone(), discovered_ids, HashSet::new())
248 }
249
250 fn from_parts(
251 discovered_ids: Vec<u64>,
252 routable_ids: Vec<u64>,
253 overloaded_ids: HashSet<u64>,
254 ) -> Self {
255 let free_ids = Self::derive_free_ids(&routable_ids, &overloaded_ids);
256 Self {
257 discovered_ids,
258 routable_ids,
259 overloaded_ids,
260 free_ids,
261 }
262 }
263
264 pub(crate) fn discovered_ids(&self) -> &[u64] {
265 &self.discovered_ids
266 }
267
268 pub(crate) fn routable_ids(&self) -> &[u64] {
269 &self.routable_ids
270 }
271
272 pub(crate) fn free_ids(&self) -> &[u64] {
273 &self.free_ids
274 }
275
276 pub(crate) fn counts(&self) -> RoutingInstanceCounts {
277 RoutingInstanceCounts {
278 discovered: self.discovered_ids.len(),
279 routable: self.routable_ids.len(),
280 overloaded: self.overloaded_ids.len(),
281 free: self.free_ids.len(),
282 }
283 }
284
285 pub(crate) fn is_overloaded(&self, instance_id: u64) -> bool {
286 self.overloaded_ids.contains(&instance_id)
287 }
288
289 fn overloaded_ids(&self) -> Option<HashSet<u64>> {
290 if self.overloaded_ids.is_empty() {
291 return None;
292 }
293
294 Some(self.overloaded_ids.clone())
295 }
296
297 fn reconcile_discovered(&self, discovered_ids: Vec<u64>) -> Self {
298 let old_discovered_ids = self.discovered_ids.iter().copied().collect::<HashSet<_>>();
299 let new_discovered_ids = discovered_ids.iter().copied().collect::<HashSet<_>>();
300 let mut overloaded_ids = self.overloaded_ids.clone();
301 overloaded_ids
302 .retain(|id| !old_discovered_ids.contains(id) || new_discovered_ids.contains(id));
303
304 Self::from_parts(discovered_ids.clone(), discovered_ids, overloaded_ids)
305 }
306
307 fn report_instance_down(&self, instance_id: u64) -> Self {
308 let routable_ids: Vec<u64> = self
309 .routable_ids
310 .iter()
311 .copied()
312 .filter(|id| *id != instance_id)
313 .collect();
314
315 Self::from_parts(
316 self.discovered_ids.clone(),
317 routable_ids,
318 self.overloaded_ids.clone(),
319 )
320 }
321
322 #[cfg(test)]
323 fn override_routable_ids(&self, routable_ids: Vec<u64>) -> Self {
324 Self::from_parts(
327 self.discovered_ids.clone(),
328 routable_ids,
329 self.overloaded_ids.clone(),
330 )
331 }
332
333 fn set_overloaded(&self, overloaded_ids: HashSet<u64>) -> Self {
334 Self::from_parts(
335 self.discovered_ids.clone(),
336 self.routable_ids.clone(),
337 overloaded_ids,
338 )
339 }
340
341 fn mark_overloaded(&self, instance_id: u64) -> Self {
345 let mut overloaded_ids = self.overloaded_ids.clone();
346 overloaded_ids.insert(instance_id);
347 Self::from_parts(
348 self.discovered_ids.clone(),
349 self.routable_ids.clone(),
350 overloaded_ids,
351 )
352 }
353
354 fn clear_overloaded_for_removed(&self, removed_ids: &HashSet<u64>) -> Self {
355 let mut overloaded_ids = self.overloaded_ids.clone();
356 overloaded_ids.retain(|id| !removed_ids.contains(id));
357 Self::from_parts(
358 self.discovered_ids.clone(),
359 self.routable_ids.clone(),
360 overloaded_ids,
361 )
362 }
363
364 fn derive_free_ids(routable_ids: &[u64], overloaded_ids: &HashSet<u64>) -> Vec<u64> {
365 if overloaded_ids.is_empty() {
366 return routable_ids.to_vec();
367 }
368
369 routable_ids
370 .iter()
371 .copied()
372 .filter(|id| !overloaded_ids.contains(id))
373 .collect()
374 }
375}
376
377#[derive(Debug)]
378struct RoutingInstancesState {
379 snapshot: ArcSwap<RoutingInstances>,
380 update_lock: StdMutex<()>,
381 instance_avail_tx: tokio::sync::watch::Sender<Vec<u64>>,
382}
383
384impl RoutingInstancesState {
385 fn new(discovered_ids: Vec<u64>) -> (Self, tokio::sync::watch::Receiver<Vec<u64>>) {
386 let snapshot = RoutingInstances::new(discovered_ids);
387 let (instance_avail_tx, instance_avail_rx) =
388 tokio::sync::watch::channel(snapshot.routable_ids().to_vec());
389 (
390 Self {
391 snapshot: ArcSwap::from_pointee(snapshot),
392 update_lock: StdMutex::new(()),
393 instance_avail_tx,
394 },
395 instance_avail_rx,
396 )
397 }
398
399 fn snapshot(&self) -> arc_swap::Guard<Arc<RoutingInstances>> {
400 self.snapshot.load()
401 }
402
403 fn update(
404 &self,
405 update: impl FnOnce(&RoutingInstances) -> RoutingInstances,
406 publish_routable_ids: bool,
407 ) -> Arc<RoutingInstances> {
408 let _guard = self.update_lock.lock().unwrap();
409 let current = self.snapshot.load();
410 let next = Arc::new(update(¤t));
411 self.snapshot.store(next.clone());
412 if publish_routable_ids {
413 self.publish_routable_ids(&next);
414 }
415 next
416 }
417
418 fn publish_routable_ids(&self, routing_instances: &RoutingInstances) {
419 let _ = self
420 .instance_avail_tx
421 .send(routing_instances.routable_ids().to_vec());
422 }
423
424 fn routable_ids(&self) -> Vec<u64> {
425 self.snapshot().routable_ids().to_vec()
426 }
427
428 fn free_ids(&self) -> Vec<u64> {
429 self.snapshot().free_ids.clone()
430 }
431
432 fn counts(&self) -> RoutingInstanceCounts {
433 self.snapshot().counts()
434 }
435
436 fn overloaded_ids(&self) -> Option<HashSet<u64>> {
437 self.snapshot().overloaded_ids()
438 }
439
440 fn report_instance_down(&self, instance_id: u64) {
441 self.update(|current| current.report_instance_down(instance_id), true);
442 }
443
444 fn set_overloaded_instances(&self, overloaded_instance_ids: &[u64]) -> bool {
445 let overloaded_ids = overloaded_instance_ids
446 .iter()
447 .copied()
448 .collect::<HashSet<_>>();
449 let _guard = self.update_lock.lock().unwrap();
450 let current = self.snapshot.load();
451 if current.overloaded_ids == overloaded_ids {
452 return false;
453 }
454
455 let next = Arc::new(current.set_overloaded(overloaded_ids));
456 self.snapshot.store(next);
457 true
458 }
459
460 fn mark_overloaded_immediate(&self, instance_id: u64) {
461 self.update(
462 move |current| current.mark_overloaded(instance_id),
463 false,
466 );
467 }
468
469 fn clear_overloaded_for_removed(&self, removed_instance_ids: &[u64]) {
470 if removed_instance_ids.is_empty() {
471 return;
472 }
473
474 let removed_ids = removed_instance_ids.iter().copied().collect::<HashSet<_>>();
475 self.update(
476 move |current| current.clear_overloaded_for_removed(&removed_ids),
477 false,
478 );
479 }
480
481 fn reconcile_discovered(&self, discovered_ids: Vec<u64>) -> Arc<RoutingInstances> {
482 self.update(
483 move |current| current.reconcile_discovered(discovered_ids),
484 true,
485 )
486 }
487
488 #[cfg(test)]
489 fn override_routable_ids(&self, ids: Vec<u64>) {
490 self.update(move |current| current.override_routable_ids(ids), true);
491 }
492}
493
494#[derive(Clone, Debug)]
495pub struct Client {
496 pub endpoint: Endpoint,
498 endpoint_discovery_source: Arc<EndpointDiscoverySource>,
500 pub instance_source: Arc<tokio::sync::watch::Receiver<Vec<Instance>>>,
502 routing_instances: Arc<RoutingInstancesState>,
504 instance_avail_owner: Arc<tokio::sync::watch::Receiver<Vec<u64>>>,
506 reconcile_interval: Duration,
510}
511
512impl Client {
513 pub(crate) async fn new(endpoint: Endpoint) -> Result<Self> {
515 Self::with_reconcile_interval(endpoint, *INHIBITED_DURATION).await
516 }
517
518 pub(crate) async fn with_reconcile_interval(
522 endpoint: Endpoint,
523 reconcile_interval: Duration,
524 ) -> Result<Self> {
525 tracing::trace!(
526 "Client::new_dynamic: Creating dynamic client for endpoint: {}",
527 endpoint.id()
528 );
529 let endpoint_discovery_source =
530 Self::get_or_create_dynamic_discovery_source(&endpoint).await?;
531 let instance_source = Arc::new(endpoint_discovery_source.instance_receiver());
532
533 let initial_ids: Vec<u64> = instance_source
538 .borrow()
539 .iter()
540 .map(|instance| instance.id())
541 .collect();
542 let (routing_instances, instance_avail_owner) = RoutingInstancesState::new(initial_ids);
543 let client = Client {
544 endpoint: endpoint.clone(),
545 endpoint_discovery_source,
546 instance_source: instance_source.clone(),
547 routing_instances: Arc::new(routing_instances),
548 instance_avail_owner: Arc::new(instance_avail_owner),
549 reconcile_interval,
550 };
551 client.monitor_instance_source();
552 Ok(client)
553 }
554
555 pub fn instances(&self) -> Vec<Instance> {
557 self.instance_source.borrow().clone()
558 }
559
560 pub fn instance_ids(&self) -> Vec<u64> {
561 self.instances().into_iter().map(|ep| ep.id()).collect()
562 }
563
564 pub fn instance_ids_avail(&self) -> Vec<u64> {
565 self.routing_instances.routable_ids()
566 }
567
568 pub fn instance_ids_free(&self) -> Vec<u64> {
571 self.routing_instances.free_ids()
572 }
573
574 pub(crate) fn routing_instances(&self) -> arc_swap::Guard<Arc<RoutingInstances>> {
575 self.routing_instances.snapshot()
576 }
577
578 pub fn routing_instance_counts(&self) -> RoutingInstanceCounts {
579 self.routing_instances.counts()
580 }
581
582 pub fn instance_avail_watcher(&self) -> tokio::sync::watch::Receiver<Vec<u64>> {
584 self.instance_avail_owner.as_ref().clone()
585 }
586
587 pub(crate) fn subscribe_discovery_events(&self) -> DiscoveryEventReceiver {
592 DiscoveryEventReceiver {
593 receiver: self.endpoint_discovery_source.subscribe_events(),
594 _source: self.endpoint_discovery_source.clone(),
595 }
596 }
597
598 pub async fn wait_for_instances(&self) -> Result<Vec<Instance>> {
600 tracing::trace!(
601 "wait_for_instances: Starting wait for endpoint: {}",
602 self.endpoint.id()
603 );
604 let mut rx = self.instance_source.as_ref().clone();
605 let mut instances: Vec<Instance>;
607 loop {
608 instances = rx.borrow_and_update().to_vec();
609 if instances.is_empty() {
610 rx.changed().await?;
611 } else {
612 tracing::info!(
613 "wait_for_instances: Found {} instance(s) for endpoint: {}",
614 instances.len(),
615 self.endpoint.id()
616 );
617 break;
618 }
619 }
620 Ok(instances)
621 }
622
623 pub fn report_instance_down(&self, instance_id: u64) {
625 if self.reconcile_interval.is_zero() {
626 tracing::debug!(
627 instance_id,
628 "local worker inhibition is disabled; leaving instance routable"
629 );
630 return;
631 }
632
633 self.routing_instances.report_instance_down(instance_id);
634 tracing::debug!("inhibiting instance {instance_id}");
635 }
636
637 pub fn set_overloaded_instances(&self, overloaded_instance_ids: &[u64]) -> bool {
640 self.routing_instances
641 .set_overloaded_instances(overloaded_instance_ids)
642 }
643
644 pub fn mark_overloaded_immediate(&self, instance_id: u64) {
649 self.routing_instances
650 .mark_overloaded_immediate(instance_id);
651 tracing::debug!(
652 instance_id,
653 "marking instance overloaded (backpressure); next metric event will re-evaluate"
654 );
655 }
656
657 pub fn clear_overloaded_instances_for_removed(&self, removed_instance_ids: &[u64]) {
658 self.routing_instances
659 .clear_overloaded_for_removed(removed_instance_ids);
660 }
661
662 pub fn overloaded_instance_ids(&self) -> Option<HashSet<u64>> {
663 self.routing_instances.overloaded_ids()
664 }
665
666 fn monitor_instance_source(&self) {
673 let reconcile_interval = self.reconcile_interval;
674 let cancel_token = self.endpoint.drt().primary_token();
675 let endpoint = self.endpoint.clone();
676 let endpoint_discovery_source = self.endpoint_discovery_source.clone();
677 let routing_instances = self.routing_instances.clone();
678 let instance_source = self.instance_source.clone();
679 let endpoint_id = self.endpoint.id();
680 tokio::task::spawn(async move {
681 let mut rx = instance_source.as_ref().clone();
682 while !cancel_token.is_cancelled() {
683 let instance_ids: Vec<u64> = rx
684 .borrow_and_update()
685 .iter()
686 .map(|instance| instance.id())
687 .collect();
688
689 let snapshot = routing_instances.reconcile_discovered(instance_ids);
690
691 let registry = endpoint.drt().routing_occupancy_states();
693 if let Ok(registry) = registry.try_lock()
694 && let Some(weak) = registry.get(&endpoint)
695 && let Some(state) = weak.upgrade()
696 {
697 state.retain(snapshot.discovered_ids());
698 }
699
700 tokio::select! {
701 _ = cancel_token.cancelled() => break,
702 _ = routing_instances.instance_avail_tx.closed() => break,
703 result = rx.changed() => {
704 if let Err(err) = result {
705 tracing::error!(
706 "monitor_instance_source: The Sender is dropped: {err}, endpoint={endpoint_id}",
707 );
708 cancel_token.cancel();
709 }
710 }
711 _ = tokio::time::sleep(reconcile_interval), if !reconcile_interval.is_zero() => {
712 tracing::trace!(
713 "monitor_instance_source: periodic reconciliation for endpoint={endpoint_id}",
714 );
715 }
716 }
717 }
718 drop(endpoint_discovery_source);
719 });
720 }
721
722 #[cfg(test)]
725 pub(crate) fn override_instance_avail(&self, ids: Vec<u64>) {
726 self.routing_instances.override_routable_ids(ids);
727 }
728
729 async fn get_or_create_dynamic_discovery_source(
730 endpoint: &Endpoint,
731 ) -> Result<Arc<EndpointDiscoverySource>> {
732 let drt = endpoint.drt();
733 let sources = drt.endpoint_discovery_sources();
734 let mut sources = sources.lock().await;
735
736 if let Some(source) = sources.get(endpoint) {
737 if let Some(source) = source.upgrade() {
738 return Ok(source);
739 } else {
740 sources.remove(endpoint);
741 }
742 }
743
744 let discovery = drt.discovery();
745 let discovery_query = crate::discovery::DiscoveryQuery::Endpoint {
746 namespace: endpoint.component.namespace.name.clone(),
747 component: endpoint.component.name.clone(),
748 endpoint: endpoint.name.clone(),
749 };
750
751 let mut discovery_stream = discovery
752 .list_and_watch(discovery_query.clone(), None)
753 .await?;
754 let (watch_tx, watch_rx) = tokio::sync::watch::channel(vec![]);
755 let discovery_source = Arc::new(EndpointDiscoverySource::new(watch_rx));
756
757 let secondary = endpoint.component.drt.runtime().secondary().clone();
758 let discovery_source_task = Arc::downgrade(&discovery_source);
759
760 secondary.spawn(async move {
761 tracing::trace!("endpoint_watcher: Starting for discovery query: {:?}", discovery_query);
762 let mut map: HashMap<u64, Instance> = HashMap::new();
763
764 loop {
765 let discovery_event = tokio::select! {
766 _ = watch_tx.closed() => {
767 break;
768 }
769 discovery_event = discovery_stream.next() => {
770 match discovery_event {
771 Some(Ok(event)) => {
772 event
773 },
774 Some(Err(e)) => {
775 tracing::error!("endpoint_watcher: discovery stream error: {}; shutting down for discovery query: {:?}", e, discovery_query);
776 break;
777 }
778 None => {
779 break;
780 }
781 }
782 }
783 };
784
785 if let Some(discovery_source) = discovery_source_task.upgrade() {
786 discovery_source.broadcast_event(&discovery_event);
787 }
788
789 match discovery_event {
790 DiscoveryEvent::Added(DiscoveryInstance::Endpoint(instance)) => {
791 map.insert(instance.instance_id, instance);
792 }
793 DiscoveryEvent::Added(_) => {}
794 DiscoveryEvent::Removed(id) => {
795 if let DiscoveryInstanceId::Endpoint(endpoint_id) = id {
796 map.remove(&endpoint_id.instance_id);
797 }
798 }
799 }
800
801 let instances: Vec<Instance> = map.values().cloned().collect();
802 if watch_tx.send(instances).is_err() {
803 break;
804 }
805 }
806 let _ = watch_tx.send(vec![]);
807 });
808
809 sources.insert(endpoint.clone(), Arc::downgrade(&discovery_source));
810 Ok(discovery_source)
811 }
812}
813
814#[cfg(test)]
815mod tests {
816 use super::*;
817 use crate::{DistributedRuntime, Runtime, distributed::DistributedConfig};
818
819 async fn wait_for_discovery_event(
820 receiver: &mut DiscoveryEventReceiver,
821 predicate: impl Fn(&DiscoveryEvent) -> bool,
822 ) {
823 tokio::time::timeout(Duration::from_secs(1), async {
824 loop {
825 let event = receiver.recv().await.expect("discovery event feed closed");
826 if predicate(&event) {
827 return;
828 }
829 }
830 })
831 .await
832 .expect("expected discovery event was not received");
833 }
834
835 async fn wait_for_watch_state<T>(
836 receiver: &mut tokio::sync::watch::Receiver<Vec<T>>,
837 predicate: impl Fn(&[T]) -> bool,
838 ) {
839 tokio::time::timeout(Duration::from_secs(1), async {
840 loop {
841 if predicate(receiver.borrow_and_update().as_slice()) {
842 return;
843 }
844 receiver
845 .changed()
846 .await
847 .expect("instance availability feed closed");
848 }
849 })
850 .await
851 .expect("expected instance availability state was not observed");
852 }
853
854 #[test]
855 fn test_inhibited_duration_from_env() {
856 assert_eq!(
857 inhibited_duration_from_env(|_| None),
858 Duration::from_secs(DEFAULT_INHIBITED_DURATION_SECS)
859 );
860 assert_eq!(
861 inhibited_duration_from_env(|_| Some("17".to_string())),
862 Duration::from_secs(17)
863 );
864 assert_eq!(
865 inhibited_duration_from_env(|_| Some("0".to_string())),
866 Duration::ZERO
867 );
868 assert_eq!(
869 inhibited_duration_from_env(|_| Some("invalid".to_string())),
870 Duration::from_secs(DEFAULT_INHIBITED_DURATION_SECS)
871 );
872 }
873
874 #[tokio::test]
875 async fn dropping_last_client_releases_routing_state() {
876 let rt = Runtime::from_current().unwrap();
877 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
878 .await
879 .unwrap();
880 let endpoint = drt
881 .namespace("test_client_lifecycle".to_string())
882 .unwrap()
883 .component("test_component".to_string())
884 .unwrap()
885 .endpoint("decode".to_string());
886 let client = endpoint.client().await.unwrap();
887 let routing_instances = Arc::downgrade(&client.routing_instances);
888 let discovery_source = Arc::downgrade(&client.endpoint_discovery_source);
889 let mut raw_watcher = client.instance_source.as_ref().clone();
890 let mut watcher = client.instance_avail_watcher();
891 let mut event_watcher = client.subscribe_discovery_events();
892 raw_watcher.borrow_and_update();
893 watcher.borrow_and_update();
894
895 drop(client);
896 assert!(routing_instances.upgrade().is_some());
897 assert!(discovery_source.upgrade().is_some());
898
899 endpoint.register_endpoint_instance().await.unwrap();
900 wait_for_watch_state(&mut watcher, |instances| instances.len() == 1).await;
901 wait_for_discovery_event(&mut event_watcher, |event| {
902 matches!(event, DiscoveryEvent::Added(DiscoveryInstance::Endpoint(_)))
903 })
904 .await;
905
906 endpoint.unregister_endpoint_instance().await.unwrap();
907 wait_for_watch_state(&mut watcher, |instances| instances.is_empty()).await;
908 assert!(raw_watcher.borrow_and_update().is_empty());
909 wait_for_discovery_event(&mut event_watcher, |event| {
910 matches!(event, DiscoveryEvent::Removed(_))
911 })
912 .await;
913
914 drop(watcher);
915
916 tokio::time::timeout(Duration::from_secs(1), async {
917 while routing_instances.strong_count() != 0 {
918 tokio::task::yield_now().await;
919 }
920 })
921 .await
922 .expect("client monitor retained state after its last observer was dropped");
923 assert!(discovery_source.upgrade().is_some());
924
925 endpoint.register_endpoint_instance().await.unwrap();
926 wait_for_watch_state(&mut raw_watcher, |instances| instances.len() == 1).await;
927 wait_for_discovery_event(&mut event_watcher, |event| {
928 matches!(event, DiscoveryEvent::Added(DiscoveryInstance::Endpoint(_)))
929 })
930 .await;
931 endpoint.unregister_endpoint_instance().await.unwrap();
932 wait_for_watch_state(&mut raw_watcher, |instances| instances.is_empty()).await;
933 wait_for_discovery_event(&mut event_watcher, |event| {
934 matches!(event, DiscoveryEvent::Removed(_))
935 })
936 .await;
937
938 drop(event_watcher);
939 tokio::time::timeout(Duration::from_secs(1), async {
940 while discovery_source.strong_count() != 0 {
941 tokio::task::yield_now().await;
942 }
943 })
944 .await
945 .expect("discovery event receiver retained its source after being dropped");
946
947 rt.shutdown();
948 }
949
950 #[tokio::test]
953 async fn test_instance_reconciliation() {
954 const TEST_RECONCILE_INTERVAL: Duration = Duration::from_millis(100);
955
956 let rt = Runtime::from_current().unwrap();
957 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
959 .await
960 .unwrap();
961 let ns = drt.namespace("test_reconciliation".to_string()).unwrap();
962 let component = ns.component("test_component".to_string()).unwrap();
963 let endpoint = component.endpoint("test_endpoint".to_string());
964
965 let client = Client::with_reconcile_interval(endpoint, TEST_RECONCILE_INTERVAL)
967 .await
968 .unwrap();
969
970 assert!(client.instance_ids_avail().is_empty());
972
973 client.override_instance_avail(vec![1, 2, 3]);
976
977 assert_eq!(client.instance_ids_avail(), vec![1u64, 2, 3]);
978
979 client.report_instance_down(2);
981 assert_eq!(client.instance_ids_avail(), vec![1u64, 3]);
982
983 tokio::time::sleep(TEST_RECONCILE_INTERVAL + Duration::from_millis(50)).await;
987
988 assert!(
990 client.instance_ids_avail().is_empty(),
991 "After reconciliation, instance_avail should match instance_source"
992 );
993
994 rt.shutdown();
995 }
996
997 #[tokio::test]
999 async fn test_zero_inhibited_duration_leaves_instance_routable() {
1000 let rt = Runtime::from_current().unwrap();
1001 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1002 .await
1003 .unwrap();
1004 let ns = drt
1005 .namespace("test_disabled_inhibition".to_string())
1006 .unwrap();
1007 let component = ns.component("test_component".to_string()).unwrap();
1008 let endpoint = component.endpoint("test_endpoint".to_string());
1009
1010 let client = Client::with_reconcile_interval(endpoint, Duration::ZERO)
1011 .await
1012 .unwrap();
1013
1014 client.override_instance_avail(vec![1, 2, 3]);
1015 client.report_instance_down(2);
1016
1017 assert_eq!(
1018 client.instance_ids_avail(),
1019 vec![1, 2, 3],
1020 "a zero inhibited duration should leave the reported instance routable"
1021 );
1022
1023 rt.shutdown();
1024 }
1025
1026 #[tokio::test]
1028 async fn test_report_instance_down() {
1029 let rt = Runtime::from_current().unwrap();
1030 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1032 .await
1033 .unwrap();
1034 let ns = drt.namespace("test_report_down".to_string()).unwrap();
1035 let component = ns.component("test_component".to_string()).unwrap();
1036 let endpoint = component.endpoint("test_endpoint".to_string());
1037
1038 let client = endpoint.client().await.unwrap();
1039
1040 client.override_instance_avail(vec![1, 2, 3]);
1042 assert_eq!(client.instance_ids_avail(), vec![1u64, 2, 3]);
1043
1044 client.report_instance_down(2);
1046
1047 let avail = client.instance_ids_avail();
1049 assert!(avail.contains(&1), "Instance 1 should still be available");
1050 assert!(
1051 !avail.contains(&2),
1052 "Instance 2 should be removed after report_instance_down"
1053 );
1054 assert!(avail.contains(&3), "Instance 3 should still be available");
1055
1056 rt.shutdown();
1057 }
1058
1059 #[tokio::test]
1060 async fn test_overloaded_instance_ids_returns_none_when_empty() {
1061 let rt = Runtime::from_current().unwrap();
1062 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1063 .await
1064 .unwrap();
1065 let ns = drt.namespace("test_overloaded_ids".to_string()).unwrap();
1066 let component = ns.component("test_component".to_string()).unwrap();
1067 let endpoint = component.endpoint("test_endpoint".to_string());
1068 let client = endpoint.client().await.unwrap();
1069
1070 assert_eq!(client.overloaded_instance_ids(), None);
1071
1072 assert!(client.set_overloaded_instances(&[7]));
1073 assert_eq!(client.overloaded_instance_ids(), Some(HashSet::from([7])));
1074 assert!(!client.set_overloaded_instances(&[7]));
1075
1076 assert!(client.set_overloaded_instances(&[]));
1077 assert_eq!(client.overloaded_instance_ids(), None);
1078 assert!(!client.set_overloaded_instances(&[]));
1079
1080 rt.shutdown();
1081 }
1082
1083 #[tokio::test]
1084 async fn test_instance_reconciliation_preserves_overloaded_existing_instances() {
1085 const TEST_RECONCILE_INTERVAL: Duration = Duration::from_millis(50);
1086
1087 let rt = Runtime::from_current().unwrap();
1088 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1089 .await
1090 .unwrap();
1091 let ns = drt
1092 .namespace("test_overloaded_reconciliation".to_string())
1093 .unwrap();
1094 let component = ns.component("test_component".to_string()).unwrap();
1095 let endpoint = component.endpoint("test_endpoint".to_string());
1096
1097 let client = Client::with_reconcile_interval(endpoint.clone(), TEST_RECONCILE_INTERVAL)
1098 .await
1099 .unwrap();
1100 endpoint.register_endpoint_instance().await.unwrap();
1101 let instances = client.wait_for_instances().await.unwrap();
1102 let worker_id = instances[0].id();
1103
1104 for _ in 0..10 {
1105 if client.instance_ids_free().contains(&worker_id) {
1106 break;
1107 }
1108 tokio::time::sleep(TEST_RECONCILE_INTERVAL).await;
1109 }
1110 assert!(
1111 client.instance_ids_free().contains(&worker_id),
1112 "worker should be free after initial discovery reconciliation"
1113 );
1114
1115 client.set_overloaded_instances(&[worker_id]);
1116 assert!(
1117 client.instance_ids_free().is_empty(),
1118 "worker should be overloaded before periodic reconciliation"
1119 );
1120
1121 tokio::time::sleep(TEST_RECONCILE_INTERVAL + Duration::from_millis(50)).await;
1122
1123 assert!(
1124 client.instance_ids_free().is_empty(),
1125 "periodic reconciliation should not mark an existing overloaded worker free"
1126 );
1127
1128 rt.shutdown();
1129 }
1130
1131 #[tokio::test]
1132 async fn test_report_instance_down_preserves_overloaded_state() {
1133 const TEST_RECONCILE_INTERVAL: Duration = Duration::from_millis(50);
1134
1135 let rt = Runtime::from_current().unwrap();
1136 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1137 .await
1138 .unwrap();
1139 let ns = drt
1140 .namespace("test_report_down_preserves_overloaded".to_string())
1141 .unwrap();
1142 let component = ns.component("test_component".to_string()).unwrap();
1143 let endpoint = component.endpoint("test_endpoint".to_string());
1144
1145 let client = Client::with_reconcile_interval(endpoint.clone(), TEST_RECONCILE_INTERVAL)
1146 .await
1147 .unwrap();
1148 endpoint.register_endpoint_instance().await.unwrap();
1149 let instances = client.wait_for_instances().await.unwrap();
1150 let worker_id = instances[0].id();
1151
1152 for _ in 0..10 {
1153 if client.instance_ids_avail().contains(&worker_id) {
1154 break;
1155 }
1156 tokio::time::sleep(TEST_RECONCILE_INTERVAL).await;
1157 }
1158
1159 client.set_overloaded_instances(&[worker_id]);
1160 client.report_instance_down(worker_id);
1161
1162 assert!(
1163 !client.instance_ids_avail().contains(&worker_id),
1164 "reported-down worker should leave routable availability"
1165 );
1166 assert_eq!(
1167 client.routing_instance_counts().overloaded,
1168 1,
1169 "reported-down worker should remain overloaded while still discovered"
1170 );
1171 assert!(
1172 client.instance_ids_free().is_empty(),
1173 "reported-down overloaded worker should not become free"
1174 );
1175
1176 endpoint.unregister_endpoint_instance().await.unwrap();
1177 for _ in 0..10 {
1178 if client.routing_instance_counts().overloaded == 0 {
1179 break;
1180 }
1181 tokio::time::sleep(TEST_RECONCILE_INTERVAL).await;
1182 }
1183
1184 assert_eq!(
1185 client.routing_instance_counts().overloaded,
1186 0,
1187 "stable discovery removal should clear overloaded state"
1188 );
1189
1190 rt.shutdown();
1191 }
1192
1193 #[tokio::test]
1194 async fn test_instance_reconciliation_prunes_removed_overloaded_instances() {
1195 const TEST_RECONCILE_INTERVAL: Duration = Duration::from_millis(50);
1196
1197 let rt = Runtime::from_current().unwrap();
1198 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1199 .await
1200 .unwrap();
1201 let ns = drt
1202 .namespace("test_removed_overloaded_cleanup".to_string())
1203 .unwrap();
1204 let component = ns.component("test_component".to_string()).unwrap();
1205 let endpoint = component.endpoint("test_endpoint".to_string());
1206
1207 let client = Client::with_reconcile_interval(endpoint.clone(), TEST_RECONCILE_INTERVAL)
1208 .await
1209 .unwrap();
1210 endpoint.register_endpoint_instance().await.unwrap();
1211 let instances = client.wait_for_instances().await.unwrap();
1212 let worker_id = instances[0].id();
1213
1214 client.set_overloaded_instances(&[worker_id]);
1215 assert_eq!(client.routing_instance_counts().overloaded, 1);
1216 assert!(client.instance_ids_free().is_empty());
1217
1218 endpoint.unregister_endpoint_instance().await.unwrap();
1219 for _ in 0..10 {
1220 if client.routing_instance_counts().overloaded == 0 {
1221 break;
1222 }
1223 tokio::time::sleep(TEST_RECONCILE_INTERVAL).await;
1224 }
1225
1226 assert_eq!(
1227 client.routing_instance_counts().overloaded,
1228 0,
1229 "removed discovered workers should not remain in overloaded state"
1230 );
1231
1232 rt.shutdown();
1233 }
1234
1235 #[tokio::test]
1236 async fn test_instance_ids_free_excludes_overloaded_new_instances() {
1237 const TEST_RECONCILE_INTERVAL: Duration = Duration::from_millis(50);
1238
1239 let rt = Runtime::from_current().unwrap();
1240 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1241 .await
1242 .unwrap();
1243 let worker_id = drt.connection_id();
1244 let ns = drt
1245 .namespace("test_new_overloaded_reconciliation".to_string())
1246 .unwrap();
1247 let component = ns.component("test_component".to_string()).unwrap();
1248 let endpoint = component.endpoint("test_endpoint".to_string());
1249
1250 let client = Client::with_reconcile_interval(endpoint.clone(), TEST_RECONCILE_INTERVAL)
1251 .await
1252 .unwrap();
1253 client.set_overloaded_instances(&[worker_id]);
1254
1255 endpoint.register_endpoint_instance().await.unwrap();
1256 let instances = client.wait_for_instances().await.unwrap();
1257 assert_eq!(instances[0].id(), worker_id);
1258 assert!(
1259 client.instance_ids_free().is_empty(),
1260 "newly discovered overloaded worker should not be free"
1261 );
1262
1263 tokio::time::sleep(TEST_RECONCILE_INTERVAL + Duration::from_millis(50)).await;
1264
1265 assert!(
1266 client.instance_ids_free().is_empty(),
1267 "discovery reconciliation should not affect recomputed free workers"
1268 );
1269
1270 rt.shutdown();
1271 }
1272
1273 #[tokio::test]
1274 async fn test_discovery_add_updates_free_without_overloaded_publish() {
1275 const TEST_RECONCILE_INTERVAL: Duration = Duration::from_millis(50);
1276
1277 let rt = Runtime::from_current().unwrap();
1278 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1279 .await
1280 .unwrap();
1281 let ns = drt
1282 .namespace("test_free_updates_on_discovery_add".to_string())
1283 .unwrap();
1284 let component = ns.component("test_component".to_string()).unwrap();
1285 let endpoint = component.endpoint("test_endpoint".to_string());
1286
1287 let client = Client::with_reconcile_interval(endpoint.clone(), TEST_RECONCILE_INTERVAL)
1288 .await
1289 .unwrap();
1290 endpoint.register_endpoint_instance().await.unwrap();
1291 let instances = client.wait_for_instances().await.unwrap();
1292 let worker_id = instances[0].id();
1293
1294 for _ in 0..10 {
1295 if client.instance_ids_free().contains(&worker_id) {
1296 break;
1297 }
1298 tokio::time::sleep(TEST_RECONCILE_INTERVAL).await;
1299 }
1300
1301 assert_eq!(
1302 client.instance_ids_free(),
1303 vec![worker_id],
1304 "newly discovered non-overloaded workers should appear free without an overload update"
1305 );
1306
1307 rt.shutdown();
1308 }
1309
1310 #[tokio::test]
1312 async fn test_instance_avail_watcher() {
1313 let rt = Runtime::from_current().unwrap();
1314 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1316 .await
1317 .unwrap();
1318 let ns = drt.namespace("test_watcher".to_string()).unwrap();
1319 let component = ns.component("test_component".to_string()).unwrap();
1320 let endpoint = component.endpoint("test_endpoint".to_string());
1321
1322 let client = endpoint.client().await.unwrap();
1323 let watcher = client.instance_avail_watcher();
1324
1325 client.override_instance_avail(vec![1, 2, 3]);
1327
1328 client.report_instance_down(2);
1330
1331 let current = watcher.borrow().clone();
1334 assert_eq!(current, vec![1, 3]);
1335
1336 rt.shutdown();
1337 }
1338
1339 #[tokio::test]
1341 async fn test_concurrent_select_and_increment() {
1342 let state = Arc::new(RoutingOccupancyState::default());
1343 let instance_ids: Vec<u64> = vec![100, 200, 300];
1344 let num_requests = 90;
1345
1346 let mut handles = Vec::new();
1347 for _ in 0..num_requests {
1348 let state = state.clone();
1349 let ids = instance_ids.clone();
1350 handles.push(tokio::spawn(async move {
1351 state.select_exact_min_and_increment(&ids).await
1352 }));
1353 }
1354
1355 for handle in handles {
1356 handle.await.unwrap();
1357 }
1358
1359 assert_eq!(state.load(100), 30);
1360 assert_eq!(state.load(200), 30);
1361 assert_eq!(state.load(300), 30);
1362 }
1363
1364 #[tokio::test]
1365 async fn test_select_exact_min_and_increment_randomizes_ties() {
1366 let mut selected = [false; 3];
1367
1368 for _ in 0..120 {
1369 let state = RoutingOccupancyState::default();
1370 let picked = state
1371 .select_exact_min_and_increment(&[10, 20, 30])
1372 .await
1373 .unwrap();
1374 match picked {
1375 10 => selected[0] = true,
1376 20 => selected[1] = true,
1377 30 => selected[2] = true,
1378 _ => panic!("unexpected worker id: {picked}"),
1379 }
1380 }
1381
1382 let selected_count = selected.into_iter().filter(|seen| *seen).count();
1383 assert!(
1384 selected_count > 1,
1385 "tie-breaking should not always select the first minimum-load worker"
1386 );
1387 }
1388
1389 #[tokio::test]
1390 async fn test_connection_counts() {
1391 let rt = Runtime::from_current().unwrap();
1392 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1393 .await
1394 .unwrap();
1395 let ns = drt.namespace("test_ll_counts".to_string()).unwrap();
1396 let component = ns.component("test_component".to_string()).unwrap();
1397 let endpoint = component.endpoint("test_endpoint".to_string());
1398
1399 let state1 = get_or_create_routing_occupancy_state(&endpoint).await;
1400 let state2 = get_or_create_routing_occupancy_state(&endpoint).await;
1401
1402 let picked1 = state1
1403 .select_exact_min_and_increment(&[10, 20, 30])
1404 .await
1405 .unwrap();
1406 assert_eq!(state1.load(picked1), 1);
1407
1408 let picked2 = state1
1409 .select_exact_min_and_increment(&[10, 20, 30])
1410 .await
1411 .unwrap();
1412 assert_ne!(picked1, picked2);
1413
1414 assert_eq!(state2.load(10), state1.load(10));
1416 assert_eq!(state2.load(20), state1.load(20));
1417 assert_eq!(state2.load(30), state1.load(30));
1418
1419 state2.decrement(picked1);
1420 assert_eq!(state1.load(picked1), if picked1 == picked2 { 1 } else { 0 });
1421
1422 rt.shutdown();
1423 }
1424
1425 #[tokio::test]
1426 async fn test_least_loaded_state_retain() {
1427 let state = RoutingOccupancyState::default();
1428
1429 state.select_exact_min_and_increment(&[1, 2, 3]).await;
1431 state.select_exact_min_and_increment(&[1, 2, 3]).await;
1432 state.select_exact_min_and_increment(&[1, 2, 3]).await;
1433 assert_eq!(state.load(1), 1);
1435 assert_eq!(state.load(2), 1);
1436 assert_eq!(state.load(3), 1);
1437
1438 state.retain(&[1, 3]);
1440
1441 assert_eq!(state.load(1), 1);
1442 assert_eq!(state.load(2), 0);
1443 assert_eq!(state.load(3), 1);
1444 }
1445
1446 #[tokio::test]
1447 async fn test_monitor_instance_source_cleans_up_removed_worker_counts() {
1448 const TEST_RECONCILE_INTERVAL: Duration = Duration::from_millis(50);
1449
1450 let rt = Runtime::from_current().unwrap();
1451 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1452 .await
1453 .unwrap();
1454 let ns = drt.namespace("test_occupancy_cleanup".to_string()).unwrap();
1455 let component = ns.component("test_component".to_string()).unwrap();
1456 let endpoint = component.endpoint("test_endpoint".to_string());
1457
1458 let client = Client::with_reconcile_interval(endpoint.clone(), TEST_RECONCILE_INTERVAL)
1459 .await
1460 .unwrap();
1461 endpoint.register_endpoint_instance().await.unwrap();
1462 client.wait_for_instances().await.unwrap();
1463
1464 let worker_id = client.instance_ids_avail()[0];
1465 let state = get_or_create_routing_occupancy_state(&endpoint).await;
1466 state.increment(worker_id);
1467 assert_eq!(state.load(worker_id), 1);
1468
1469 endpoint.unregister_endpoint_instance().await.unwrap();
1470
1471 for _ in 0..10 {
1472 if state.load(worker_id) == 0 {
1473 break;
1474 }
1475 tokio::time::sleep(TEST_RECONCILE_INTERVAL).await;
1476 }
1477
1478 assert_eq!(state.load(worker_id), 0);
1479
1480 rt.shutdown();
1481 }
1482}