1use std::sync::atomic::{AtomicBool, 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 overload_reconciliation_needed: AtomicBool,
382 instance_avail_tx: tokio::sync::watch::Sender<Vec<u64>>,
383}
384
385impl RoutingInstancesState {
386 fn new(discovered_ids: Vec<u64>) -> (Self, tokio::sync::watch::Receiver<Vec<u64>>) {
387 let snapshot = RoutingInstances::new(discovered_ids);
388 let (instance_avail_tx, instance_avail_rx) =
389 tokio::sync::watch::channel(snapshot.routable_ids().to_vec());
390 (
391 Self {
392 snapshot: ArcSwap::from_pointee(snapshot),
393 update_lock: StdMutex::new(()),
394 overload_reconciliation_needed: AtomicBool::new(false),
395 instance_avail_tx,
396 },
397 instance_avail_rx,
398 )
399 }
400
401 fn snapshot(&self) -> arc_swap::Guard<Arc<RoutingInstances>> {
402 self.snapshot.load()
403 }
404
405 fn update(
406 &self,
407 update: impl FnOnce(&RoutingInstances) -> RoutingInstances,
408 publish_routable_ids: bool,
409 ) -> Arc<RoutingInstances> {
410 let _guard = self.update_lock.lock().unwrap();
411 let current = self.snapshot.load();
412 let next = Arc::new(update(¤t));
413 self.snapshot.store(next.clone());
414 if publish_routable_ids {
415 self.publish_routable_ids(&next);
416 }
417 next
418 }
419
420 fn publish_routable_ids(&self, routing_instances: &RoutingInstances) {
421 let _ = self
422 .instance_avail_tx
423 .send(routing_instances.routable_ids().to_vec());
424 }
425
426 fn routable_ids(&self) -> Vec<u64> {
427 self.snapshot().routable_ids().to_vec()
428 }
429
430 fn free_ids(&self) -> Vec<u64> {
431 self.snapshot().free_ids.clone()
432 }
433
434 fn counts(&self) -> RoutingInstanceCounts {
435 self.snapshot().counts()
436 }
437
438 fn overloaded_ids(&self) -> Option<HashSet<u64>> {
439 self.snapshot().overloaded_ids()
440 }
441
442 fn report_instance_down(&self, instance_id: u64) {
443 self.update(|current| current.report_instance_down(instance_id), true);
444 }
445
446 fn set_overloaded_instances(&self, overloaded_instance_ids: &[u64]) -> bool {
447 let overloaded_ids = overloaded_instance_ids
448 .iter()
449 .copied()
450 .collect::<HashSet<_>>();
451 let _guard = self.update_lock.lock().unwrap();
452 self.overload_reconciliation_needed
453 .store(false, Ordering::Release);
454 let current = self.snapshot.load();
455 if current.overloaded_ids == overloaded_ids {
456 return false;
457 }
458
459 let next = Arc::new(current.set_overloaded(overloaded_ids));
460 self.snapshot.store(next);
461 true
462 }
463
464 fn mark_overloaded_immediate(&self, instance_id: u64) {
465 let _guard = self.update_lock.lock().unwrap();
466 let current = self.snapshot.load();
467 let next = Arc::new(current.mark_overloaded(instance_id));
468 self.snapshot.store(next);
469 self.overload_reconciliation_needed
470 .store(true, Ordering::Release);
471 }
472
473 fn overload_reconciliation_needed(&self) -> bool {
474 self.overload_reconciliation_needed.load(Ordering::Acquire)
475 }
476
477 fn clear_overloaded_for_removed(&self, removed_instance_ids: &[u64]) {
478 if removed_instance_ids.is_empty() {
479 return;
480 }
481
482 let removed_ids = removed_instance_ids.iter().copied().collect::<HashSet<_>>();
483 self.update(
484 move |current| current.clear_overloaded_for_removed(&removed_ids),
485 false,
486 );
487 }
488
489 fn reconcile_discovered(&self, discovered_ids: Vec<u64>) -> Arc<RoutingInstances> {
490 self.update(
491 move |current| current.reconcile_discovered(discovered_ids),
492 true,
493 )
494 }
495
496 #[cfg(test)]
497 fn override_routable_ids(&self, ids: Vec<u64>) {
498 self.update(move |current| current.override_routable_ids(ids), true);
499 }
500}
501
502#[derive(Clone, Debug)]
503pub struct Client {
504 pub endpoint: Endpoint,
506 endpoint_discovery_source: Arc<EndpointDiscoverySource>,
508 pub instance_source: Arc<tokio::sync::watch::Receiver<Vec<Instance>>>,
510 routing_instances: Arc<RoutingInstancesState>,
512 instance_avail_owner: Arc<tokio::sync::watch::Receiver<Vec<u64>>>,
514 reconcile_interval: Duration,
518}
519
520impl Client {
521 pub(crate) async fn new(endpoint: Endpoint) -> Result<Self> {
523 Self::with_reconcile_interval(endpoint, *INHIBITED_DURATION).await
524 }
525
526 pub(crate) async fn with_reconcile_interval(
530 endpoint: Endpoint,
531 reconcile_interval: Duration,
532 ) -> Result<Self> {
533 tracing::trace!(
534 "Client::new_dynamic: Creating dynamic client for endpoint: {}",
535 endpoint.id()
536 );
537 let endpoint_discovery_source =
538 Self::get_or_create_dynamic_discovery_source(&endpoint).await?;
539 let instance_source = Arc::new(endpoint_discovery_source.instance_receiver());
540
541 let initial_ids: Vec<u64> = instance_source
546 .borrow()
547 .iter()
548 .map(|instance| instance.id())
549 .collect();
550 let (routing_instances, instance_avail_owner) = RoutingInstancesState::new(initial_ids);
551 let client = Client {
552 endpoint: endpoint.clone(),
553 endpoint_discovery_source,
554 instance_source: instance_source.clone(),
555 routing_instances: Arc::new(routing_instances),
556 instance_avail_owner: Arc::new(instance_avail_owner),
557 reconcile_interval,
558 };
559 client.monitor_instance_source();
560 Ok(client)
561 }
562
563 pub fn instances(&self) -> Vec<Instance> {
565 self.instance_source.borrow().clone()
566 }
567
568 pub fn instance_ids(&self) -> Vec<u64> {
569 self.instances().into_iter().map(|ep| ep.id()).collect()
570 }
571
572 pub fn instance_ids_avail(&self) -> Vec<u64> {
573 self.routing_instances.routable_ids()
574 }
575
576 pub fn instance_ids_free(&self) -> Vec<u64> {
579 self.routing_instances.free_ids()
580 }
581
582 pub(crate) fn routing_instances(&self) -> arc_swap::Guard<Arc<RoutingInstances>> {
583 self.routing_instances.snapshot()
584 }
585
586 pub fn routing_instance_counts(&self) -> RoutingInstanceCounts {
587 self.routing_instances.counts()
588 }
589
590 pub fn instance_avail_watcher(&self) -> tokio::sync::watch::Receiver<Vec<u64>> {
592 self.instance_avail_owner.as_ref().clone()
593 }
594
595 pub(crate) fn subscribe_discovery_events(&self) -> DiscoveryEventReceiver {
600 DiscoveryEventReceiver {
601 receiver: self.endpoint_discovery_source.subscribe_events(),
602 _source: self.endpoint_discovery_source.clone(),
603 }
604 }
605
606 pub async fn wait_for_instances(&self) -> Result<Vec<Instance>> {
608 tracing::trace!(
609 "wait_for_instances: Starting wait for endpoint: {}",
610 self.endpoint.id()
611 );
612 let mut rx = self.instance_source.as_ref().clone();
613 let mut instances: Vec<Instance>;
615 loop {
616 instances = rx.borrow_and_update().to_vec();
617 if instances.is_empty() {
618 rx.changed().await?;
619 } else {
620 tracing::info!(
621 "wait_for_instances: Found {} instance(s) for endpoint: {}",
622 instances.len(),
623 self.endpoint.id()
624 );
625 break;
626 }
627 }
628 Ok(instances)
629 }
630
631 pub fn report_instance_down(&self, instance_id: u64) {
633 if self.reconcile_interval.is_zero() {
634 tracing::debug!(
635 instance_id,
636 "local worker inhibition is disabled; leaving instance routable"
637 );
638 return;
639 }
640
641 self.routing_instances.report_instance_down(instance_id);
642 tracing::debug!("inhibiting instance {instance_id}");
643 }
644
645 pub fn set_overloaded_instances(&self, overloaded_instance_ids: &[u64]) -> bool {
648 self.routing_instances
649 .set_overloaded_instances(overloaded_instance_ids)
650 }
651
652 pub fn overload_reconciliation_needed(&self) -> bool {
655 self.routing_instances.overload_reconciliation_needed()
656 }
657
658 pub fn mark_overloaded_immediate(&self, instance_id: u64) {
663 self.routing_instances
664 .mark_overloaded_immediate(instance_id);
665 tracing::debug!(
666 instance_id,
667 "marking instance overloaded (backpressure); next metric event will re-evaluate"
668 );
669 }
670
671 pub fn clear_overloaded_instances_for_removed(&self, removed_instance_ids: &[u64]) {
672 self.routing_instances
673 .clear_overloaded_for_removed(removed_instance_ids);
674 }
675
676 pub fn overloaded_instance_ids(&self) -> Option<HashSet<u64>> {
677 self.routing_instances.overloaded_ids()
678 }
679
680 fn monitor_instance_source(&self) {
687 let reconcile_interval = self.reconcile_interval;
688 let cancel_token = self.endpoint.drt().primary_token();
689 let endpoint = self.endpoint.clone();
690 let endpoint_discovery_source = self.endpoint_discovery_source.clone();
691 let routing_instances = self.routing_instances.clone();
692 let instance_source = self.instance_source.clone();
693 let endpoint_id = self.endpoint.id();
694 tokio::task::spawn(async move {
695 let mut rx = instance_source.as_ref().clone();
696 while !cancel_token.is_cancelled() {
697 let instance_ids: Vec<u64> = rx
698 .borrow_and_update()
699 .iter()
700 .map(|instance| instance.id())
701 .collect();
702
703 let snapshot = routing_instances.reconcile_discovered(instance_ids);
704
705 let registry = endpoint.drt().routing_occupancy_states();
707 if let Ok(registry) = registry.try_lock()
708 && let Some(weak) = registry.get(&endpoint)
709 && let Some(state) = weak.upgrade()
710 {
711 state.retain(snapshot.discovered_ids());
712 }
713
714 tokio::select! {
715 _ = cancel_token.cancelled() => break,
716 _ = routing_instances.instance_avail_tx.closed() => break,
717 result = rx.changed() => {
718 if let Err(err) = result {
719 tracing::error!(
720 "monitor_instance_source: The Sender is dropped: {err}, endpoint={endpoint_id}",
721 );
722 cancel_token.cancel();
723 }
724 }
725 _ = tokio::time::sleep(reconcile_interval), if !reconcile_interval.is_zero() => {
726 tracing::trace!(
727 "monitor_instance_source: periodic reconciliation for endpoint={endpoint_id}",
728 );
729 }
730 }
731 }
732 drop(endpoint_discovery_source);
733 });
734 }
735
736 #[cfg(test)]
739 pub(crate) fn override_instance_avail(&self, ids: Vec<u64>) {
740 self.routing_instances.override_routable_ids(ids);
741 }
742
743 async fn get_or_create_dynamic_discovery_source(
744 endpoint: &Endpoint,
745 ) -> Result<Arc<EndpointDiscoverySource>> {
746 let drt = endpoint.drt();
747 let sources = drt.endpoint_discovery_sources();
748 let mut sources = sources.lock().await;
749
750 if let Some(source) = sources.get(endpoint) {
751 if let Some(source) = source.upgrade() {
752 return Ok(source);
753 } else {
754 sources.remove(endpoint);
755 }
756 }
757
758 let discovery = drt.discovery();
759 let discovery_query = crate::discovery::DiscoveryQuery::Endpoint {
760 namespace: endpoint.component.namespace.name.clone(),
761 component: endpoint.component.name.clone(),
762 endpoint: endpoint.name.clone(),
763 };
764
765 let mut discovery_stream = discovery
766 .list_and_watch(discovery_query.clone(), None)
767 .await?;
768 let (watch_tx, watch_rx) = tokio::sync::watch::channel(vec![]);
769 let discovery_source = Arc::new(EndpointDiscoverySource::new(watch_rx));
770
771 let secondary = endpoint.component.drt.runtime().secondary().clone();
772 let discovery_source_task = Arc::downgrade(&discovery_source);
773
774 secondary.spawn(async move {
775 tracing::trace!("endpoint_watcher: Starting for discovery query: {:?}", discovery_query);
776 let mut map: HashMap<u64, Instance> = HashMap::new();
777
778 loop {
779 let discovery_event = tokio::select! {
780 _ = watch_tx.closed() => {
781 break;
782 }
783 discovery_event = discovery_stream.next() => {
784 match discovery_event {
785 Some(Ok(event)) => {
786 event
787 },
788 Some(Err(e)) => {
789 tracing::error!("endpoint_watcher: discovery stream error: {}; shutting down for discovery query: {:?}", e, discovery_query);
790 break;
791 }
792 None => {
793 break;
794 }
795 }
796 }
797 };
798
799 if let Some(discovery_source) = discovery_source_task.upgrade() {
800 discovery_source.broadcast_event(&discovery_event);
801 }
802
803 match discovery_event {
804 DiscoveryEvent::Added(DiscoveryInstance::Endpoint(instance)) => {
805 map.insert(instance.instance_id, instance);
806 }
807 DiscoveryEvent::Added(_) => {}
808 DiscoveryEvent::Removed(id) => {
809 if let DiscoveryInstanceId::Endpoint(endpoint_id) = id {
810 map.remove(&endpoint_id.instance_id);
811 }
812 }
813 }
814
815 let instances: Vec<Instance> = map.values().cloned().collect();
816 if watch_tx.send(instances).is_err() {
817 break;
818 }
819 }
820 let _ = watch_tx.send(vec![]);
821 });
822
823 sources.insert(endpoint.clone(), Arc::downgrade(&discovery_source));
824 Ok(discovery_source)
825 }
826}
827
828#[cfg(test)]
829mod tests {
830 use super::*;
831 use crate::{DistributedRuntime, Runtime, distributed::DistributedConfig};
832
833 async fn wait_for_discovery_event(
834 receiver: &mut DiscoveryEventReceiver,
835 predicate: impl Fn(&DiscoveryEvent) -> bool,
836 ) {
837 tokio::time::timeout(Duration::from_secs(1), async {
838 loop {
839 let event = receiver.recv().await.expect("discovery event feed closed");
840 if predicate(&event) {
841 return;
842 }
843 }
844 })
845 .await
846 .expect("expected discovery event was not received");
847 }
848
849 async fn wait_for_watch_state<T>(
850 receiver: &mut tokio::sync::watch::Receiver<Vec<T>>,
851 predicate: impl Fn(&[T]) -> bool,
852 ) {
853 tokio::time::timeout(Duration::from_secs(1), async {
854 loop {
855 if predicate(receiver.borrow_and_update().as_slice()) {
856 return;
857 }
858 receiver
859 .changed()
860 .await
861 .expect("instance availability feed closed");
862 }
863 })
864 .await
865 .expect("expected instance availability state was not observed");
866 }
867
868 #[test]
869 fn test_inhibited_duration_from_env() {
870 assert_eq!(
871 inhibited_duration_from_env(|_| None),
872 Duration::from_secs(DEFAULT_INHIBITED_DURATION_SECS)
873 );
874 assert_eq!(
875 inhibited_duration_from_env(|_| Some("17".to_string())),
876 Duration::from_secs(17)
877 );
878 assert_eq!(
879 inhibited_duration_from_env(|_| Some("0".to_string())),
880 Duration::ZERO
881 );
882 assert_eq!(
883 inhibited_duration_from_env(|_| Some("invalid".to_string())),
884 Duration::from_secs(DEFAULT_INHIBITED_DURATION_SECS)
885 );
886 }
887
888 #[tokio::test]
889 async fn dropping_last_client_releases_routing_state() {
890 let rt = Runtime::from_current().unwrap();
891 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
892 .await
893 .unwrap();
894 let endpoint = drt
895 .namespace("test_client_lifecycle".to_string())
896 .unwrap()
897 .component("test_component".to_string())
898 .unwrap()
899 .endpoint("decode".to_string());
900 let client = endpoint.client().await.unwrap();
901 let routing_instances = Arc::downgrade(&client.routing_instances);
902 let discovery_source = Arc::downgrade(&client.endpoint_discovery_source);
903 let mut raw_watcher = client.instance_source.as_ref().clone();
904 let mut watcher = client.instance_avail_watcher();
905 let mut event_watcher = client.subscribe_discovery_events();
906 raw_watcher.borrow_and_update();
907 watcher.borrow_and_update();
908
909 drop(client);
910 assert!(routing_instances.upgrade().is_some());
911 assert!(discovery_source.upgrade().is_some());
912
913 endpoint.register_endpoint_instance().await.unwrap();
914 wait_for_watch_state(&mut watcher, |instances| instances.len() == 1).await;
915 wait_for_discovery_event(&mut event_watcher, |event| {
916 matches!(event, DiscoveryEvent::Added(DiscoveryInstance::Endpoint(_)))
917 })
918 .await;
919
920 endpoint.unregister_endpoint_instance().await.unwrap();
921 wait_for_watch_state(&mut watcher, |instances| instances.is_empty()).await;
922 assert!(raw_watcher.borrow_and_update().is_empty());
923 wait_for_discovery_event(&mut event_watcher, |event| {
924 matches!(event, DiscoveryEvent::Removed(_))
925 })
926 .await;
927
928 drop(watcher);
929
930 tokio::time::timeout(Duration::from_secs(1), async {
931 while routing_instances.strong_count() != 0 {
932 tokio::task::yield_now().await;
933 }
934 })
935 .await
936 .expect("client monitor retained state after its last observer was dropped");
937 assert!(discovery_source.upgrade().is_some());
938
939 endpoint.register_endpoint_instance().await.unwrap();
940 wait_for_watch_state(&mut raw_watcher, |instances| instances.len() == 1).await;
941 wait_for_discovery_event(&mut event_watcher, |event| {
942 matches!(event, DiscoveryEvent::Added(DiscoveryInstance::Endpoint(_)))
943 })
944 .await;
945 endpoint.unregister_endpoint_instance().await.unwrap();
946 wait_for_watch_state(&mut raw_watcher, |instances| instances.is_empty()).await;
947 wait_for_discovery_event(&mut event_watcher, |event| {
948 matches!(event, DiscoveryEvent::Removed(_))
949 })
950 .await;
951
952 drop(event_watcher);
953 tokio::time::timeout(Duration::from_secs(1), async {
954 while discovery_source.strong_count() != 0 {
955 tokio::task::yield_now().await;
956 }
957 })
958 .await
959 .expect("discovery event receiver retained its source after being dropped");
960
961 rt.shutdown();
962 }
963
964 #[tokio::test]
967 async fn test_instance_reconciliation() {
968 const TEST_RECONCILE_INTERVAL: Duration = Duration::from_millis(100);
969
970 let rt = Runtime::from_current().unwrap();
971 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
973 .await
974 .unwrap();
975 let ns = drt.namespace("test_reconciliation".to_string()).unwrap();
976 let component = ns.component("test_component".to_string()).unwrap();
977 let endpoint = component.endpoint("test_endpoint".to_string());
978
979 let client = Client::with_reconcile_interval(endpoint, TEST_RECONCILE_INTERVAL)
981 .await
982 .unwrap();
983
984 assert!(client.instance_ids_avail().is_empty());
986
987 client.override_instance_avail(vec![1, 2, 3]);
990
991 assert_eq!(client.instance_ids_avail(), vec![1u64, 2, 3]);
992
993 client.report_instance_down(2);
995 assert_eq!(client.instance_ids_avail(), vec![1u64, 3]);
996
997 tokio::time::sleep(TEST_RECONCILE_INTERVAL + Duration::from_millis(50)).await;
1001
1002 assert!(
1004 client.instance_ids_avail().is_empty(),
1005 "After reconciliation, instance_avail should match instance_source"
1006 );
1007
1008 rt.shutdown();
1009 }
1010
1011 #[tokio::test]
1013 async fn test_zero_inhibited_duration_leaves_instance_routable() {
1014 let rt = Runtime::from_current().unwrap();
1015 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1016 .await
1017 .unwrap();
1018 let ns = drt
1019 .namespace("test_disabled_inhibition".to_string())
1020 .unwrap();
1021 let component = ns.component("test_component".to_string()).unwrap();
1022 let endpoint = component.endpoint("test_endpoint".to_string());
1023
1024 let client = Client::with_reconcile_interval(endpoint, Duration::ZERO)
1025 .await
1026 .unwrap();
1027
1028 client.override_instance_avail(vec![1, 2, 3]);
1029 client.report_instance_down(2);
1030
1031 assert_eq!(
1032 client.instance_ids_avail(),
1033 vec![1, 2, 3],
1034 "a zero inhibited duration should leave the reported instance routable"
1035 );
1036
1037 rt.shutdown();
1038 }
1039
1040 #[tokio::test]
1042 async fn test_report_instance_down() {
1043 let rt = Runtime::from_current().unwrap();
1044 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1046 .await
1047 .unwrap();
1048 let ns = drt.namespace("test_report_down".to_string()).unwrap();
1049 let component = ns.component("test_component".to_string()).unwrap();
1050 let endpoint = component.endpoint("test_endpoint".to_string());
1051
1052 let client = endpoint.client().await.unwrap();
1053
1054 client.override_instance_avail(vec![1, 2, 3]);
1056 assert_eq!(client.instance_ids_avail(), vec![1u64, 2, 3]);
1057
1058 client.report_instance_down(2);
1060
1061 let avail = client.instance_ids_avail();
1063 assert!(avail.contains(&1), "Instance 1 should still be available");
1064 assert!(
1065 !avail.contains(&2),
1066 "Instance 2 should be removed after report_instance_down"
1067 );
1068 assert!(avail.contains(&3), "Instance 3 should still be available");
1069
1070 rt.shutdown();
1071 }
1072
1073 #[tokio::test]
1074 async fn test_overloaded_instance_ids_returns_none_when_empty() {
1075 let rt = Runtime::from_current().unwrap();
1076 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1077 .await
1078 .unwrap();
1079 let ns = drt.namespace("test_overloaded_ids".to_string()).unwrap();
1080 let component = ns.component("test_component".to_string()).unwrap();
1081 let endpoint = component.endpoint("test_endpoint".to_string());
1082 let client = endpoint.client().await.unwrap();
1083
1084 assert_eq!(client.overloaded_instance_ids(), None);
1085
1086 assert!(client.set_overloaded_instances(&[7]));
1087 assert_eq!(client.overloaded_instance_ids(), Some(HashSet::from([7])));
1088 assert!(!client.set_overloaded_instances(&[7]));
1089
1090 assert!(client.set_overloaded_instances(&[]));
1091 assert_eq!(client.overloaded_instance_ids(), None);
1092 assert!(!client.set_overloaded_instances(&[]));
1093
1094 rt.shutdown();
1095 }
1096
1097 #[tokio::test]
1098 async fn test_instance_reconciliation_preserves_overloaded_existing_instances() {
1099 const TEST_RECONCILE_INTERVAL: Duration = Duration::from_millis(50);
1100
1101 let rt = Runtime::from_current().unwrap();
1102 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1103 .await
1104 .unwrap();
1105 let ns = drt
1106 .namespace("test_overloaded_reconciliation".to_string())
1107 .unwrap();
1108 let component = ns.component("test_component".to_string()).unwrap();
1109 let endpoint = component.endpoint("test_endpoint".to_string());
1110
1111 let client = Client::with_reconcile_interval(endpoint.clone(), TEST_RECONCILE_INTERVAL)
1112 .await
1113 .unwrap();
1114 endpoint.register_endpoint_instance().await.unwrap();
1115 let instances = client.wait_for_instances().await.unwrap();
1116 let worker_id = instances[0].id();
1117
1118 for _ in 0..10 {
1119 if client.instance_ids_free().contains(&worker_id) {
1120 break;
1121 }
1122 tokio::time::sleep(TEST_RECONCILE_INTERVAL).await;
1123 }
1124 assert!(
1125 client.instance_ids_free().contains(&worker_id),
1126 "worker should be free after initial discovery reconciliation"
1127 );
1128
1129 client.set_overloaded_instances(&[worker_id]);
1130 assert!(
1131 client.instance_ids_free().is_empty(),
1132 "worker should be overloaded before periodic reconciliation"
1133 );
1134
1135 tokio::time::sleep(TEST_RECONCILE_INTERVAL + Duration::from_millis(50)).await;
1136
1137 assert!(
1138 client.instance_ids_free().is_empty(),
1139 "periodic reconciliation should not mark an existing overloaded worker free"
1140 );
1141
1142 rt.shutdown();
1143 }
1144
1145 #[tokio::test]
1146 async fn test_report_instance_down_preserves_overloaded_state() {
1147 const TEST_RECONCILE_INTERVAL: Duration = Duration::from_millis(50);
1148
1149 let rt = Runtime::from_current().unwrap();
1150 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1151 .await
1152 .unwrap();
1153 let ns = drt
1154 .namespace("test_report_down_preserves_overloaded".to_string())
1155 .unwrap();
1156 let component = ns.component("test_component".to_string()).unwrap();
1157 let endpoint = component.endpoint("test_endpoint".to_string());
1158
1159 let client = Client::with_reconcile_interval(endpoint.clone(), TEST_RECONCILE_INTERVAL)
1160 .await
1161 .unwrap();
1162 endpoint.register_endpoint_instance().await.unwrap();
1163 let instances = client.wait_for_instances().await.unwrap();
1164 let worker_id = instances[0].id();
1165
1166 for _ in 0..10 {
1167 if client.instance_ids_avail().contains(&worker_id) {
1168 break;
1169 }
1170 tokio::time::sleep(TEST_RECONCILE_INTERVAL).await;
1171 }
1172
1173 client.set_overloaded_instances(&[worker_id]);
1174 client.report_instance_down(worker_id);
1175
1176 assert!(
1177 !client.instance_ids_avail().contains(&worker_id),
1178 "reported-down worker should leave routable availability"
1179 );
1180 assert_eq!(
1181 client.routing_instance_counts().overloaded,
1182 1,
1183 "reported-down worker should remain overloaded while still discovered"
1184 );
1185 assert!(
1186 client.instance_ids_free().is_empty(),
1187 "reported-down overloaded worker should not become free"
1188 );
1189
1190 endpoint.unregister_endpoint_instance().await.unwrap();
1191 for _ in 0..10 {
1192 if client.routing_instance_counts().overloaded == 0 {
1193 break;
1194 }
1195 tokio::time::sleep(TEST_RECONCILE_INTERVAL).await;
1196 }
1197
1198 assert_eq!(
1199 client.routing_instance_counts().overloaded,
1200 0,
1201 "stable discovery removal should clear overloaded state"
1202 );
1203
1204 rt.shutdown();
1205 }
1206
1207 #[tokio::test]
1208 async fn test_instance_reconciliation_prunes_removed_overloaded_instances() {
1209 const TEST_RECONCILE_INTERVAL: Duration = Duration::from_millis(50);
1210
1211 let rt = Runtime::from_current().unwrap();
1212 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1213 .await
1214 .unwrap();
1215 let ns = drt
1216 .namespace("test_removed_overloaded_cleanup".to_string())
1217 .unwrap();
1218 let component = ns.component("test_component".to_string()).unwrap();
1219 let endpoint = component.endpoint("test_endpoint".to_string());
1220
1221 let client = Client::with_reconcile_interval(endpoint.clone(), TEST_RECONCILE_INTERVAL)
1222 .await
1223 .unwrap();
1224 endpoint.register_endpoint_instance().await.unwrap();
1225 let instances = client.wait_for_instances().await.unwrap();
1226 let worker_id = instances[0].id();
1227
1228 client.set_overloaded_instances(&[worker_id]);
1229 assert_eq!(client.routing_instance_counts().overloaded, 1);
1230 assert!(client.instance_ids_free().is_empty());
1231
1232 endpoint.unregister_endpoint_instance().await.unwrap();
1233 for _ in 0..10 {
1234 if client.routing_instance_counts().overloaded == 0 {
1235 break;
1236 }
1237 tokio::time::sleep(TEST_RECONCILE_INTERVAL).await;
1238 }
1239
1240 assert_eq!(
1241 client.routing_instance_counts().overloaded,
1242 0,
1243 "removed discovered workers should not remain in overloaded state"
1244 );
1245
1246 rt.shutdown();
1247 }
1248
1249 #[tokio::test]
1250 async fn test_instance_ids_free_excludes_overloaded_new_instances() {
1251 const TEST_RECONCILE_INTERVAL: Duration = Duration::from_millis(50);
1252
1253 let rt = Runtime::from_current().unwrap();
1254 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1255 .await
1256 .unwrap();
1257 let worker_id = drt.connection_id();
1258 let ns = drt
1259 .namespace("test_new_overloaded_reconciliation".to_string())
1260 .unwrap();
1261 let component = ns.component("test_component".to_string()).unwrap();
1262 let endpoint = component.endpoint("test_endpoint".to_string());
1263
1264 let client = Client::with_reconcile_interval(endpoint.clone(), TEST_RECONCILE_INTERVAL)
1265 .await
1266 .unwrap();
1267 client.set_overloaded_instances(&[worker_id]);
1268
1269 endpoint.register_endpoint_instance().await.unwrap();
1270 let instances = client.wait_for_instances().await.unwrap();
1271 assert_eq!(instances[0].id(), worker_id);
1272 assert!(
1273 client.instance_ids_free().is_empty(),
1274 "newly discovered overloaded worker should not be free"
1275 );
1276
1277 tokio::time::sleep(TEST_RECONCILE_INTERVAL + Duration::from_millis(50)).await;
1278
1279 assert!(
1280 client.instance_ids_free().is_empty(),
1281 "discovery reconciliation should not affect recomputed free workers"
1282 );
1283
1284 rt.shutdown();
1285 }
1286
1287 #[tokio::test]
1288 async fn test_discovery_add_updates_free_without_overloaded_publish() {
1289 const TEST_RECONCILE_INTERVAL: Duration = Duration::from_millis(50);
1290
1291 let rt = Runtime::from_current().unwrap();
1292 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1293 .await
1294 .unwrap();
1295 let ns = drt
1296 .namespace("test_free_updates_on_discovery_add".to_string())
1297 .unwrap();
1298 let component = ns.component("test_component".to_string()).unwrap();
1299 let endpoint = component.endpoint("test_endpoint".to_string());
1300
1301 let client = Client::with_reconcile_interval(endpoint.clone(), TEST_RECONCILE_INTERVAL)
1302 .await
1303 .unwrap();
1304 endpoint.register_endpoint_instance().await.unwrap();
1305 let instances = client.wait_for_instances().await.unwrap();
1306 let worker_id = instances[0].id();
1307
1308 for _ in 0..10 {
1309 if client.instance_ids_free().contains(&worker_id) {
1310 break;
1311 }
1312 tokio::time::sleep(TEST_RECONCILE_INTERVAL).await;
1313 }
1314
1315 assert_eq!(
1316 client.instance_ids_free(),
1317 vec![worker_id],
1318 "newly discovered non-overloaded workers should appear free without an overload update"
1319 );
1320
1321 rt.shutdown();
1322 }
1323
1324 #[tokio::test]
1326 async fn test_instance_avail_watcher() {
1327 let rt = Runtime::from_current().unwrap();
1328 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1330 .await
1331 .unwrap();
1332 let ns = drt.namespace("test_watcher".to_string()).unwrap();
1333 let component = ns.component("test_component".to_string()).unwrap();
1334 let endpoint = component.endpoint("test_endpoint".to_string());
1335
1336 let client = endpoint.client().await.unwrap();
1337 let watcher = client.instance_avail_watcher();
1338
1339 client.override_instance_avail(vec![1, 2, 3]);
1341
1342 client.report_instance_down(2);
1344
1345 let current = watcher.borrow().clone();
1348 assert_eq!(current, vec![1, 3]);
1349
1350 rt.shutdown();
1351 }
1352
1353 #[tokio::test]
1355 async fn test_concurrent_select_and_increment() {
1356 let state = Arc::new(RoutingOccupancyState::default());
1357 let instance_ids: Vec<u64> = vec![100, 200, 300];
1358 let num_requests = 90;
1359
1360 let mut handles = Vec::new();
1361 for _ in 0..num_requests {
1362 let state = state.clone();
1363 let ids = instance_ids.clone();
1364 handles.push(tokio::spawn(async move {
1365 state.select_exact_min_and_increment(&ids).await
1366 }));
1367 }
1368
1369 for handle in handles {
1370 handle.await.unwrap();
1371 }
1372
1373 assert_eq!(state.load(100), 30);
1374 assert_eq!(state.load(200), 30);
1375 assert_eq!(state.load(300), 30);
1376 }
1377
1378 #[tokio::test]
1379 async fn test_select_exact_min_and_increment_randomizes_ties() {
1380 let mut selected = [false; 3];
1381
1382 for _ in 0..120 {
1383 let state = RoutingOccupancyState::default();
1384 let picked = state
1385 .select_exact_min_and_increment(&[10, 20, 30])
1386 .await
1387 .unwrap();
1388 match picked {
1389 10 => selected[0] = true,
1390 20 => selected[1] = true,
1391 30 => selected[2] = true,
1392 _ => panic!("unexpected worker id: {picked}"),
1393 }
1394 }
1395
1396 let selected_count = selected.into_iter().filter(|seen| *seen).count();
1397 assert!(
1398 selected_count > 1,
1399 "tie-breaking should not always select the first minimum-load worker"
1400 );
1401 }
1402
1403 #[tokio::test]
1404 async fn test_connection_counts() {
1405 let rt = Runtime::from_current().unwrap();
1406 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1407 .await
1408 .unwrap();
1409 let ns = drt.namespace("test_ll_counts".to_string()).unwrap();
1410 let component = ns.component("test_component".to_string()).unwrap();
1411 let endpoint = component.endpoint("test_endpoint".to_string());
1412
1413 let state1 = get_or_create_routing_occupancy_state(&endpoint).await;
1414 let state2 = get_or_create_routing_occupancy_state(&endpoint).await;
1415
1416 let picked1 = state1
1417 .select_exact_min_and_increment(&[10, 20, 30])
1418 .await
1419 .unwrap();
1420 assert_eq!(state1.load(picked1), 1);
1421
1422 let picked2 = state1
1423 .select_exact_min_and_increment(&[10, 20, 30])
1424 .await
1425 .unwrap();
1426 assert_ne!(picked1, picked2);
1427
1428 assert_eq!(state2.load(10), state1.load(10));
1430 assert_eq!(state2.load(20), state1.load(20));
1431 assert_eq!(state2.load(30), state1.load(30));
1432
1433 state2.decrement(picked1);
1434 assert_eq!(state1.load(picked1), if picked1 == picked2 { 1 } else { 0 });
1435
1436 rt.shutdown();
1437 }
1438
1439 #[tokio::test]
1440 async fn test_least_loaded_state_retain() {
1441 let state = RoutingOccupancyState::default();
1442
1443 state.select_exact_min_and_increment(&[1, 2, 3]).await;
1445 state.select_exact_min_and_increment(&[1, 2, 3]).await;
1446 state.select_exact_min_and_increment(&[1, 2, 3]).await;
1447 assert_eq!(state.load(1), 1);
1449 assert_eq!(state.load(2), 1);
1450 assert_eq!(state.load(3), 1);
1451
1452 state.retain(&[1, 3]);
1454
1455 assert_eq!(state.load(1), 1);
1456 assert_eq!(state.load(2), 0);
1457 assert_eq!(state.load(3), 1);
1458 }
1459
1460 #[tokio::test]
1461 async fn test_monitor_instance_source_cleans_up_removed_worker_counts() {
1462 const TEST_RECONCILE_INTERVAL: Duration = Duration::from_millis(50);
1463
1464 let rt = Runtime::from_current().unwrap();
1465 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1466 .await
1467 .unwrap();
1468 let ns = drt.namespace("test_occupancy_cleanup".to_string()).unwrap();
1469 let component = ns.component("test_component".to_string()).unwrap();
1470 let endpoint = component.endpoint("test_endpoint".to_string());
1471
1472 let client = Client::with_reconcile_interval(endpoint.clone(), TEST_RECONCILE_INTERVAL)
1473 .await
1474 .unwrap();
1475 endpoint.register_endpoint_instance().await.unwrap();
1476 client.wait_for_instances().await.unwrap();
1477
1478 let worker_id = client.instance_ids_avail()[0];
1479 let state = get_or_create_routing_occupancy_state(&endpoint).await;
1480 state.increment(worker_id);
1481 assert_eq!(state.load(worker_id), 1);
1482
1483 endpoint.unregister_endpoint_instance().await.unwrap();
1484
1485 for _ in 0..10 {
1486 if state.load(worker_id) == 0 {
1487 break;
1488 }
1489 tokio::time::sleep(TEST_RECONCILE_INTERVAL).await;
1490 }
1491
1492 assert_eq!(state.load(worker_id), 0);
1493
1494 rt.shutdown();
1495 }
1496}