1use std::{
5 collections::{HashMap, HashSet},
6 sync::{Arc, LazyLock, Mutex as StdMutex, Weak},
7 time::Duration,
8};
9
10use anyhow::Result;
11use arc_swap::ArcSwap;
12use futures::StreamExt;
13use tokio::time::Instant;
14use tokio_util::sync::{CancellationToken, DropGuard};
15
16use crate::component::{Endpoint, Instance};
17use crate::config::environment_names::runtime as env_runtime;
18use crate::discovery::{Discovery, DiscoveryEvent, DiscoveryInstance, DiscoveryInstanceId};
19use crate::routing_policy::{RoutingOccupancyState, get_or_create_routing_occupancy_state};
20use crate::traits::DistributedRuntimeProvider;
21
22const DEFAULT_INHIBITED_DURATION_SECS: u64 = 5;
24
25const REQUEST_PATH_OVERLOAD_COOLDOWN: Duration = Duration::from_secs(1);
28
29static INHIBITED_DURATION: LazyLock<Duration> =
31 LazyLock::new(|| inhibited_duration_from_env(|name| std::env::var(name).ok()));
32
33fn inhibited_duration_from_env(mut lookup: impl FnMut(&str) -> Option<String>) -> Duration {
34 let seconds = match lookup(env_runtime::DYN_RUNTIME_INHIBITED_DURATION_SECS) {
35 None => DEFAULT_INHIBITED_DURATION_SECS,
36 Some(raw) => match raw.parse::<u64>() {
37 Ok(seconds) => seconds,
38 Err(err) => {
39 tracing::warn!(
40 value = raw,
41 %err,
42 "invalid {}; using the default of {} seconds",
43 env_runtime::DYN_RUNTIME_INHIBITED_DURATION_SECS,
44 DEFAULT_INHIBITED_DURATION_SECS,
45 );
46 DEFAULT_INHIBITED_DURATION_SECS
47 }
48 },
49 };
50 Duration::from_secs(seconds)
51}
52
53#[derive(Debug)]
64pub(crate) struct EndpointDiscoverySource {
65 instance_source: tokio::sync::watch::Receiver<Vec<Instance>>,
66 event_subscribers: StdMutex<Vec<tokio::sync::mpsc::UnboundedSender<DiscoveryEvent>>>,
67 _watch_guard: DropGuard,
68}
69
70pub(crate) struct DiscoveryEventReceiver {
71 receiver: tokio::sync::mpsc::UnboundedReceiver<DiscoveryEvent>,
72 _source: Arc<EndpointDiscoverySource>,
73}
74
75impl std::ops::Deref for DiscoveryEventReceiver {
76 type Target = tokio::sync::mpsc::UnboundedReceiver<DiscoveryEvent>;
77
78 fn deref(&self) -> &Self::Target {
79 &self.receiver
80 }
81}
82
83impl std::ops::DerefMut for DiscoveryEventReceiver {
84 fn deref_mut(&mut self) -> &mut Self::Target {
85 &mut self.receiver
86 }
87}
88
89impl EndpointDiscoverySource {
90 fn new(
91 instance_source: tokio::sync::watch::Receiver<Vec<Instance>>,
92 watch_guard: DropGuard,
93 ) -> Self {
94 Self {
95 instance_source,
96 event_subscribers: StdMutex::new(Vec::new()),
97 _watch_guard: watch_guard,
98 }
99 }
100
101 fn instance_receiver(&self) -> tokio::sync::watch::Receiver<Vec<Instance>> {
102 self.instance_source.clone()
103 }
104
105 fn subscribe_events(&self) -> tokio::sync::mpsc::UnboundedReceiver<DiscoveryEvent> {
106 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
107 self.event_subscribers.lock().unwrap().push(tx);
108 rx
109 }
110
111 fn broadcast_event(&self, event: &DiscoveryEvent) {
112 let subscribers = &mut *self.event_subscribers.lock().unwrap();
113 subscribers.retain(|tx| tx.send(event.clone()).is_ok());
114 }
115}
116
117#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
118pub struct RoutingInstanceCounts {
119 pub discovered: usize,
120 pub routable: usize,
121 pub overloaded: usize,
122 pub free: usize,
124}
125
126#[derive(Clone, Debug)]
127pub(crate) struct RoutingInstances {
128 discovered_ids: Vec<u64>,
129 routable_ids: Vec<u64>,
130 monitor_overloaded_ids: HashSet<u64>,
131 request_overload_deadlines: HashMap<u64, Instant>,
132 next_request_overload_expiry: Option<Instant>,
133 overloaded_ids: HashSet<u64>,
134 free_ids: Vec<u64>,
135 routable_id_set: Arc<HashSet<u64>>,
136 availability_initialized: bool,
140}
141
142impl RoutingInstances {
143 fn new(discovered_ids: Vec<u64>) -> Self {
144 let availability_initialized = !discovered_ids.is_empty();
145 Self::from_parts(
146 discovered_ids.clone(),
147 discovered_ids,
148 HashSet::new(),
149 HashMap::new(),
150 availability_initialized,
151 )
152 }
153
154 fn from_parts(
155 mut discovered_ids: Vec<u64>,
156 mut routable_ids: Vec<u64>,
157 monitor_overloaded_ids: HashSet<u64>,
158 request_overload_deadlines: HashMap<u64, Instant>,
159 availability_initialized: bool,
160 ) -> Self {
161 discovered_ids.sort_unstable();
162 discovered_ids.dedup();
163 routable_ids.sort_unstable();
164 routable_ids.dedup();
165 let mut overloaded_ids = monitor_overloaded_ids.clone();
166 let next_request_overload_expiry = request_overload_deadlines.values().min().copied();
167 overloaded_ids.extend(request_overload_deadlines.keys().copied());
168 let free_ids = Self::derive_free_ids(&routable_ids, &overloaded_ids);
169 let routable_id_set = Arc::new(routable_ids.iter().copied().collect());
170 Self {
171 discovered_ids,
172 routable_ids,
173 monitor_overloaded_ids,
174 request_overload_deadlines,
175 next_request_overload_expiry,
176 overloaded_ids,
177 free_ids,
178 routable_id_set,
179 availability_initialized,
180 }
181 }
182
183 pub(crate) fn discovered_ids(&self) -> &[u64] {
184 &self.discovered_ids
185 }
186
187 pub(crate) fn routable_ids(&self) -> &[u64] {
188 &self.routable_ids
189 }
190
191 fn available_ids(&self) -> Option<Arc<HashSet<u64>>> {
192 self.availability_initialized
193 .then(|| Arc::clone(&self.routable_id_set))
194 }
195
196 pub(crate) fn free_ids(&self) -> &[u64] {
197 &self.free_ids
198 }
199
200 pub(crate) fn counts(&self) -> RoutingInstanceCounts {
201 RoutingInstanceCounts {
202 discovered: self.discovered_ids.len(),
203 routable: self.routable_ids.len(),
204 overloaded: self.overloaded_ids.len(),
205 free: self.free_ids.len(),
206 }
207 }
208
209 pub(crate) fn is_overloaded(&self, instance_id: u64) -> bool {
210 self.overloaded_ids.contains(&instance_id)
211 }
212
213 fn overloaded_ids(&self) -> Option<HashSet<u64>> {
214 if self.overloaded_ids.is_empty() {
215 return None;
216 }
217
218 Some(self.overloaded_ids.clone())
219 }
220
221 fn reconcile_discovered(&self, discovered_ids: Vec<u64>) -> Self {
222 let old_discovered_ids = self.discovered_ids.iter().copied().collect::<HashSet<_>>();
223 let new_discovered_ids = discovered_ids.iter().copied().collect::<HashSet<_>>();
224 let mut monitor_overloaded_ids = self.monitor_overloaded_ids.clone();
225 monitor_overloaded_ids
226 .retain(|id| !old_discovered_ids.contains(id) || new_discovered_ids.contains(id));
227 let mut request_overload_deadlines = self.request_overload_deadlines.clone();
228 request_overload_deadlines
229 .retain(|id, _| !old_discovered_ids.contains(id) || new_discovered_ids.contains(id));
230
231 let availability_initialized = self.availability_initialized || !discovered_ids.is_empty();
232 Self::from_parts(
233 discovered_ids.clone(),
234 discovered_ids,
235 monitor_overloaded_ids,
236 request_overload_deadlines,
237 availability_initialized,
238 )
239 }
240
241 fn report_instance_down(&self, instance_id: u64) -> Self {
242 let routable_ids: Vec<u64> = self
243 .routable_ids
244 .iter()
245 .copied()
246 .filter(|id| *id != instance_id)
247 .collect();
248
249 Self::from_parts(
250 self.discovered_ids.clone(),
251 routable_ids,
252 self.monitor_overloaded_ids.clone(),
253 self.request_overload_deadlines.clone(),
254 self.availability_initialized,
255 )
256 }
257
258 #[cfg(any(test, feature = "testing"))]
259 fn override_routable_ids(&self, routable_ids: Vec<u64>) -> Self {
260 Self::from_parts(
263 self.discovered_ids.clone(),
264 routable_ids,
265 self.monitor_overloaded_ids.clone(),
266 self.request_overload_deadlines.clone(),
267 self.availability_initialized,
268 )
269 }
270
271 fn set_overloaded(&self, overloaded_ids: HashSet<u64>) -> Self {
272 Self::from_parts(
273 self.discovered_ids.clone(),
274 self.routable_ids.clone(),
275 overloaded_ids,
276 HashMap::new(),
277 self.availability_initialized,
278 )
279 }
280
281 fn mark_overloaded_until(&self, instance_id: u64, deadline: Instant) -> Self {
285 let mut request_overload_deadlines = self.request_overload_deadlines.clone();
286 request_overload_deadlines.insert(instance_id, deadline);
287 Self::from_parts(
288 self.discovered_ids.clone(),
289 self.routable_ids.clone(),
290 self.monitor_overloaded_ids.clone(),
291 request_overload_deadlines,
292 self.availability_initialized,
293 )
294 }
295
296 fn has_expired_request_overload(&self, now: Instant) -> bool {
297 self.next_request_overload_expiry
298 .is_some_and(|deadline| deadline <= now)
299 }
300
301 fn clear_expired_request_overloads(&self, now: Instant) -> Self {
302 let mut request_overload_deadlines = self.request_overload_deadlines.clone();
303 request_overload_deadlines.retain(|_, deadline| *deadline > now);
304 Self::from_parts(
305 self.discovered_ids.clone(),
306 self.routable_ids.clone(),
307 self.monitor_overloaded_ids.clone(),
308 request_overload_deadlines,
309 self.availability_initialized,
310 )
311 }
312
313 fn clear_overloaded_for_removed(&self, removed_ids: &HashSet<u64>) -> Self {
314 let mut monitor_overloaded_ids = self.monitor_overloaded_ids.clone();
315 monitor_overloaded_ids.retain(|id| !removed_ids.contains(id));
316 let mut request_overload_deadlines = self.request_overload_deadlines.clone();
317 request_overload_deadlines.retain(|id, _| !removed_ids.contains(id));
318 Self::from_parts(
319 self.discovered_ids.clone(),
320 self.routable_ids.clone(),
321 monitor_overloaded_ids,
322 request_overload_deadlines,
323 self.availability_initialized,
324 )
325 }
326
327 fn derive_free_ids(routable_ids: &[u64], overloaded_ids: &HashSet<u64>) -> Vec<u64> {
328 if overloaded_ids.is_empty() {
329 return routable_ids.to_vec();
330 }
331
332 routable_ids
333 .iter()
334 .copied()
335 .filter(|id| !overloaded_ids.contains(id))
336 .collect()
337 }
338}
339
340#[derive(Debug)]
341struct RoutingInstancesState {
342 snapshot: ArcSwap<RoutingInstances>,
343 update_lock: StdMutex<()>,
344 instance_avail_tx: tokio::sync::watch::Sender<Vec<u64>>,
345}
346
347impl RoutingInstancesState {
348 fn new(discovered_ids: Vec<u64>) -> (Self, tokio::sync::watch::Receiver<Vec<u64>>) {
349 let snapshot = RoutingInstances::new(discovered_ids);
350 let (instance_avail_tx, instance_avail_rx) =
351 tokio::sync::watch::channel(snapshot.routable_ids().to_vec());
352 (
353 Self {
354 snapshot: ArcSwap::from_pointee(snapshot),
355 update_lock: StdMutex::new(()),
356 instance_avail_tx,
357 },
358 instance_avail_rx,
359 )
360 }
361
362 fn snapshot(&self) -> arc_swap::Guard<Arc<RoutingInstances>> {
363 let now = Instant::now();
364 let current = self.snapshot.load();
365 if !current.has_expired_request_overload(now) {
366 return current;
367 }
368 drop(current);
369
370 self.reconcile_expired_request_overloads(now);
371 self.snapshot.load()
372 }
373
374 fn reconcile_expired_request_overloads(&self, now: Instant) {
375 let _guard = self.update_lock.lock().unwrap();
376 let current = self.snapshot.load();
377 if !current.has_expired_request_overload(now) {
378 return;
379 }
380
381 let next = Arc::new(current.clear_expired_request_overloads(now));
382 self.snapshot.store(next);
383 }
384
385 fn update(
386 &self,
387 update: impl FnOnce(&RoutingInstances) -> RoutingInstances,
388 publish_routable_ids: bool,
389 ) -> Arc<RoutingInstances> {
390 let _guard = self.update_lock.lock().unwrap();
391 let current = self.snapshot.load();
392 let next = Arc::new(update(¤t));
393 self.snapshot.store(next.clone());
394 if publish_routable_ids {
395 self.publish_routable_ids(&next);
396 }
397 next
398 }
399
400 fn publish_routable_ids(&self, routing_instances: &RoutingInstances) {
401 let _ = self
402 .instance_avail_tx
403 .send(routing_instances.routable_ids().to_vec());
404 }
405
406 fn routable_ids(&self) -> Vec<u64> {
407 self.snapshot().routable_ids().to_vec()
408 }
409
410 fn available_ids(&self) -> Option<Arc<HashSet<u64>>> {
411 self.snapshot().available_ids()
412 }
413
414 fn free_ids(&self) -> Vec<u64> {
415 self.snapshot().free_ids.clone()
416 }
417
418 fn counts(&self) -> RoutingInstanceCounts {
419 self.snapshot().counts()
420 }
421
422 fn overloaded_ids(&self) -> Option<HashSet<u64>> {
423 self.snapshot().overloaded_ids()
424 }
425
426 fn report_instance_down(&self, instance_id: u64) {
427 self.update(|current| current.report_instance_down(instance_id), true);
428 }
429
430 fn set_overloaded_instances(&self, overloaded_instance_ids: &[u64]) -> bool {
431 let overloaded_ids = overloaded_instance_ids
432 .iter()
433 .copied()
434 .collect::<HashSet<_>>();
435 let _guard = self.update_lock.lock().unwrap();
436 let current = self.snapshot.load();
437 let next = Arc::new(current.set_overloaded(overloaded_ids));
438 let effective_changed = current.overloaded_ids != next.overloaded_ids;
439 let source_changed = current.monitor_overloaded_ids != next.monitor_overloaded_ids
440 || !current.request_overload_deadlines.is_empty();
441 if !source_changed {
442 return false;
443 }
444
445 self.snapshot.store(next);
446 effective_changed
447 }
448
449 fn mark_overloaded_immediate(&self, instance_id: u64) {
450 self.mark_overloaded_until(instance_id, Instant::now() + REQUEST_PATH_OVERLOAD_COOLDOWN);
451 }
452
453 fn mark_overloaded_until(&self, instance_id: u64, deadline: Instant) {
454 let _guard = self.update_lock.lock().unwrap();
455 let current = self.snapshot.load();
456 let next = Arc::new(current.mark_overloaded_until(instance_id, deadline));
457 self.snapshot.store(next);
458 }
459
460 fn overload_reconciliation_needed(&self) -> bool {
461 !self.snapshot().request_overload_deadlines.is_empty()
462 }
463
464 fn clear_overloaded_for_removed(&self, removed_instance_ids: &[u64]) {
465 if removed_instance_ids.is_empty() {
466 return;
467 }
468
469 let removed_ids = removed_instance_ids.iter().copied().collect::<HashSet<_>>();
470 self.update(
471 move |current| current.clear_overloaded_for_removed(&removed_ids),
472 false,
473 );
474 }
475
476 fn reconcile_discovered(&self, discovered_ids: Vec<u64>) -> Arc<RoutingInstances> {
477 self.update(
478 move |current| current.reconcile_discovered(discovered_ids),
479 true,
480 )
481 }
482
483 #[cfg(any(test, feature = "testing"))]
484 fn override_routable_ids(&self, ids: Vec<u64>) {
485 self.update(move |current| current.override_routable_ids(ids), true);
486 }
487}
488
489#[derive(Clone, Debug)]
490pub struct Client {
491 pub endpoint: Endpoint,
493 endpoint_discovery_source: Arc<EndpointDiscoverySource>,
495 pub instance_source: Arc<tokio::sync::watch::Receiver<Vec<Instance>>>,
497 routing_instances: Arc<RoutingInstancesState>,
499 instance_avail_owner: Arc<tokio::sync::watch::Receiver<Vec<u64>>>,
501 reconcile_interval: Duration,
505}
506
507impl Client {
508 pub(crate) async fn new(endpoint: Endpoint) -> Result<Self> {
510 Self::with_reconcile_interval(endpoint, *INHIBITED_DURATION).await
511 }
512
513 pub(crate) async fn with_cancellation(
518 endpoint: Endpoint,
519 cancel_token: tokio_util::sync::CancellationToken,
520 ) -> Result<Self> {
521 Self::with_reconcile_interval_and_cancellation(endpoint, *INHIBITED_DURATION, cancel_token)
522 .await
523 }
524
525 pub(crate) async fn with_reconcile_interval(
529 endpoint: Endpoint,
530 reconcile_interval: Duration,
531 ) -> Result<Self> {
532 let cancel_token = endpoint.drt().primary_token();
533 Self::with_reconcile_interval_and_cancellation(endpoint, reconcile_interval, cancel_token)
534 .await
535 }
536
537 pub(crate) async fn with_reconcile_interval_and_cancellation(
550 endpoint: Endpoint,
551 reconcile_interval: Duration,
552 cancel_token: tokio_util::sync::CancellationToken,
553 ) -> Result<Self> {
554 tracing::trace!(
555 "Client::new_dynamic: Creating dynamic client for endpoint: {}",
556 endpoint.id()
557 );
558 let endpoint_discovery_source =
559 Self::get_or_create_dynamic_discovery_source(&endpoint).await?;
560 let instance_source = Arc::new(endpoint_discovery_source.instance_receiver());
561
562 let initial_ids: Vec<u64> = instance_source
567 .borrow()
568 .iter()
569 .map(|instance| instance.id())
570 .collect();
571 let (routing_instances, instance_avail_owner) = RoutingInstancesState::new(initial_ids);
572 let client = Client {
573 endpoint: endpoint.clone(),
574 endpoint_discovery_source,
575 instance_source: instance_source.clone(),
576 routing_instances: Arc::new(routing_instances),
577 instance_avail_owner: Arc::new(instance_avail_owner),
578 reconcile_interval,
579 };
580 client.monitor_instance_source_with_cancellation(cancel_token, true);
581 Ok(client)
582 }
583
584 pub fn instances(&self) -> Vec<Instance> {
586 self.instance_source.borrow().clone()
587 }
588
589 pub(crate) fn instance_by_id(&self, instance_id: u64) -> Option<Instance> {
590 self.instance_source
591 .borrow()
592 .iter()
593 .find(|instance| instance.id() == instance_id)
594 .cloned()
595 }
596
597 pub fn instance_ids(&self) -> Vec<u64> {
598 self.instances().into_iter().map(|ep| ep.id()).collect()
599 }
600
601 pub fn is_instance_live(&self, instance_id: u64) -> bool {
604 self.instance_source
605 .borrow()
606 .iter()
607 .any(|instance| instance.id() == instance_id)
608 }
609
610 pub fn is_instance_discovered(&self, instance_id: u64) -> bool {
612 self.routing_instances
613 .snapshot()
614 .discovered_ids()
615 .binary_search(&instance_id)
616 .is_ok()
617 }
618
619 pub fn instance_ids_avail(&self) -> Vec<u64> {
620 self.routing_instances.routable_ids()
621 }
622
623 pub fn instance_ids_free(&self) -> Vec<u64> {
626 self.routing_instances.free_ids()
627 }
628
629 pub(crate) fn routing_instances(&self) -> arc_swap::Guard<Arc<RoutingInstances>> {
630 self.routing_instances.snapshot()
631 }
632
633 pub fn routing_instance_counts(&self) -> RoutingInstanceCounts {
634 self.routing_instances.counts()
635 }
636
637 pub fn instance_avail_watcher(&self) -> tokio::sync::watch::Receiver<Vec<u64>> {
639 self.instance_avail_owner.as_ref().clone()
640 }
641
642 pub fn with_admitted_instances(
651 &self,
652 admitted_ids: tokio::sync::watch::Receiver<Vec<u64>>,
653 ) -> Self {
654 self.with_admitted_instances_and_cancellation(
655 admitted_ids,
656 self.endpoint.drt().primary_token(),
657 )
658 }
659
660 pub fn with_admitted_instances_and_cancellation(
663 &self,
664 mut admitted_ids: tokio::sync::watch::Receiver<Vec<u64>>,
665 cancel_token: tokio_util::sync::CancellationToken,
666 ) -> Self {
667 let mut endpoint_instances = self.instance_source.as_ref().clone();
668 let initial = Self::filter_admitted_instances(
669 endpoint_instances.borrow().as_slice(),
670 admitted_ids.borrow().as_slice(),
671 );
672 let initial_ids = initial.iter().map(Instance::id).collect::<Vec<_>>();
673 let (instance_tx, instance_rx) = tokio::sync::watch::channel(initial);
674 let updater_cancel = cancel_token.clone();
675
676 tokio::spawn(async move {
677 loop {
678 tokio::select! {
679 _ = updater_cancel.cancelled() => break,
680 _ = instance_tx.closed() => break,
681 result = endpoint_instances.changed() => {
682 if result.is_err() {
683 break;
684 }
685 }
686 result = admitted_ids.changed() => {
687 if result.is_err() {
688 break;
689 }
690 }
691 }
692
693 let next = Self::filter_admitted_instances(
694 endpoint_instances.borrow_and_update().as_slice(),
695 admitted_ids.borrow_and_update().as_slice(),
696 );
697 let changed = *instance_tx.borrow() != next;
698 if changed && instance_tx.send(next).is_err() {
699 break;
700 }
701 }
702 });
703
704 let (routing_instances, instance_avail_owner) = RoutingInstancesState::new(initial_ids);
705 let client = Self {
706 endpoint: self.endpoint.clone(),
707 endpoint_discovery_source: self.endpoint_discovery_source.clone(),
708 instance_source: Arc::new(instance_rx),
709 routing_instances: Arc::new(routing_instances),
710 instance_avail_owner: Arc::new(instance_avail_owner),
711 reconcile_interval: self.reconcile_interval,
712 };
713 client.monitor_instance_source_with_cancellation(cancel_token, false);
714 client
715 }
716
717 fn filter_admitted_instances(instances: &[Instance], admitted_ids: &[u64]) -> Vec<Instance> {
718 if admitted_ids.is_empty() {
719 return Vec::new();
720 }
721
722 let admitted = admitted_ids.iter().copied().collect::<HashSet<_>>();
723 instances
724 .iter()
725 .filter(|instance| admitted.contains(&instance.id()))
726 .cloned()
727 .collect()
728 }
729
730 pub(crate) fn subscribe_discovery_events(&self) -> DiscoveryEventReceiver {
735 DiscoveryEventReceiver {
736 receiver: self.endpoint_discovery_source.subscribe_events(),
737 _source: self.endpoint_discovery_source.clone(),
738 }
739 }
740
741 pub async fn wait_for_instances(&self) -> Result<Vec<Instance>> {
743 tracing::trace!(
744 "wait_for_instances: Starting wait for endpoint: {}",
745 self.endpoint.id()
746 );
747 let mut rx = self.instance_source.as_ref().clone();
748 let mut instances: Vec<Instance>;
750 loop {
751 instances = rx.borrow_and_update().to_vec();
752 if instances.is_empty() {
753 rx.changed().await?;
754 } else {
755 tracing::info!(
756 "wait_for_instances: Found {} instance(s) for endpoint: {}",
757 instances.len(),
758 self.endpoint.id()
759 );
760 break;
761 }
762 }
763 Ok(instances)
764 }
765
766 pub async fn wait_for_routable_instances(&self) -> Result<Vec<Instance>> {
768 tracing::trace!(
769 endpoint = %self.endpoint.id(),
770 "Waiting for routable instances"
771 );
772 let mut discovered = self.instance_source.as_ref().clone();
773 let mut available = self.instance_avail_watcher();
774 loop {
775 let instances: Vec<Instance> = {
778 let available = available.borrow_and_update();
779 discovered
780 .borrow_and_update()
781 .iter()
782 .filter(|instance| available.contains(&instance.id()))
783 .cloned()
784 .collect()
785 };
786 if !instances.is_empty() {
787 tracing::debug!(
788 endpoint = %self.endpoint.id(),
789 instances = instances.len(),
790 "Routable instances are ready"
791 );
792 return Ok(instances);
793 }
794 tokio::select! {
795 result = discovered.changed() => result?,
796 result = available.changed() => result?,
797 }
798 }
799 }
800
801 pub fn report_instance_down(&self, instance_id: u64) {
803 if self.reconcile_interval.is_zero() {
804 tracing::debug!(
805 instance_id,
806 "local worker inhibition is disabled; leaving instance routable"
807 );
808 return;
809 }
810
811 self.routing_instances.report_instance_down(instance_id);
812 tracing::debug!("inhibiting instance {instance_id}");
813 }
814
815 pub fn set_overloaded_instances(&self, overloaded_instance_ids: &[u64]) -> bool {
818 self.routing_instances
819 .set_overloaded_instances(overloaded_instance_ids)
820 }
821
822 pub fn overload_reconciliation_needed(&self) -> bool {
825 self.routing_instances.overload_reconciliation_needed()
826 }
827
828 pub fn mark_overloaded_immediate(&self, instance_id: u64) {
833 self.routing_instances
834 .mark_overloaded_immediate(instance_id);
835 tracing::debug!(
836 instance_id,
837 "marking instance overloaded with a bounded backpressure lease"
838 );
839 }
840
841 pub fn clear_overloaded_instances_for_removed(&self, removed_instance_ids: &[u64]) {
842 self.routing_instances
843 .clear_overloaded_for_removed(removed_instance_ids);
844 }
845
846 pub fn overloaded_instance_ids(&self) -> Option<HashSet<u64>> {
847 self.routing_instances.overloaded_ids()
848 }
849
850 pub fn available_instance_ids(&self) -> Option<Arc<HashSet<u64>>> {
858 self.routing_instances.available_ids()
859 }
860
861 fn monitor_instance_source_with_cancellation(
872 &self,
873 cancel_token: tokio_util::sync::CancellationToken,
874 prune_shared_occupancy: bool,
875 ) {
876 let reconcile_interval = self.reconcile_interval;
877 let endpoint = self.endpoint.clone();
878 let endpoint_discovery_source = self.endpoint_discovery_source.clone();
879 let routing_instances = self.routing_instances.clone();
880 let instance_source = self.instance_source.clone();
881 let endpoint_id = self.endpoint.id();
882 tokio::task::spawn(async move {
883 let mut rx = instance_source.as_ref().clone();
884 while !cancel_token.is_cancelled() {
885 let instance_ids: Vec<u64> = rx
886 .borrow_and_update()
887 .iter()
888 .map(|instance| instance.id())
889 .collect();
890
891 let snapshot = routing_instances.reconcile_discovered(instance_ids);
892
893 if prune_shared_occupancy {
895 let registry = endpoint.drt().routing_occupancy_states();
896 if let Ok(registry) = registry.try_lock()
897 && let Some(weak) = registry.get(&endpoint)
898 && let Some(state) = weak.upgrade()
899 {
900 state.retain(snapshot.discovered_ids());
901 }
902 }
903
904 tokio::select! {
905 _ = cancel_token.cancelled() => break,
906 _ = routing_instances.instance_avail_tx.closed() => break,
907 result = rx.changed() => {
908 if let Err(err) = result {
909 tracing::error!(
910 "monitor_instance_source: The Sender is dropped: {err}, endpoint={endpoint_id}",
911 );
912 cancel_token.cancel();
913 }
914 }
915 _ = tokio::time::sleep(reconcile_interval), if !reconcile_interval.is_zero() => {
916 tracing::trace!(
917 "monitor_instance_source: periodic reconciliation for endpoint={endpoint_id}",
918 );
919 }
920 }
921 }
922 drop(endpoint_discovery_source);
923 });
924 }
925
926 #[cfg(any(test, feature = "testing"))]
928 pub fn override_discovered_instances(&self, ids: Vec<u64>) {
929 self.reconcile_discovered_instances(ids);
930 }
931
932 #[cfg(any(test, feature = "testing"))]
934 pub fn override_instance_avail(&self, ids: Vec<u64>) {
935 self.routing_instances.override_routable_ids(ids);
936 }
937
938 fn reconcile_discovered_instances(&self, discovered_ids: Vec<u64>) -> Arc<RoutingInstances> {
939 self.routing_instances.reconcile_discovered(discovered_ids)
940 }
941
942 async fn get_or_create_dynamic_discovery_source(
943 endpoint: &Endpoint,
944 ) -> Result<Arc<EndpointDiscoverySource>> {
945 let sources = endpoint.drt().endpoint_discovery_sources();
946
947 let existing = sources.lock().await.get(endpoint).and_then(Weak::upgrade);
948 if let Some(source) = existing {
949 return Ok(source);
950 }
951
952 let discovery = endpoint.drt().discovery();
956 let discovery_source = Self::spawn_dynamic_discovery_source(
957 endpoint,
958 discovery.as_ref(),
959 endpoint.drt().primary_token().child_token(),
960 )
961 .await?;
962
963 let mut sources = sources.lock().await;
964 if let Some(source) = sources.get(endpoint).and_then(Weak::upgrade) {
968 return Ok(source);
969 }
970 sources.insert(endpoint.clone(), Arc::downgrade(&discovery_source));
971 Ok(discovery_source)
972 }
973
974 async fn spawn_dynamic_discovery_source(
977 endpoint: &Endpoint,
978 discovery: &dyn Discovery,
979 cancel_token: CancellationToken,
980 ) -> Result<Arc<EndpointDiscoverySource>> {
981 let discovery_query = crate::discovery::DiscoveryQuery::Endpoint {
982 namespace: endpoint.component.namespace.name.clone(),
983 component: endpoint.component.name.clone(),
984 endpoint: endpoint.name.clone(),
985 };
986
987 let mut discovery_stream = discovery
988 .list_and_watch(discovery_query.clone(), Some(cancel_token.clone()))
989 .await?;
990 let (watch_tx, watch_rx) = tokio::sync::watch::channel(vec![]);
991 let discovery_source = Arc::new(EndpointDiscoverySource::new(
992 watch_rx,
993 cancel_token.drop_guard(),
994 ));
995
996 let secondary = endpoint.component.drt.runtime().secondary().clone();
997 let discovery_source_task = Arc::downgrade(&discovery_source);
998
999 secondary.spawn(async move {
1000 tracing::trace!("endpoint_watcher: Starting for discovery query: {:?}", discovery_query);
1001 let mut map: HashMap<u64, Instance> = HashMap::new();
1002
1003 loop {
1004 let discovery_event = tokio::select! {
1005 _ = watch_tx.closed() => {
1006 break;
1007 }
1008 discovery_event = discovery_stream.next() => {
1009 match discovery_event {
1010 Some(Ok(event)) => {
1011 event
1012 },
1013 Some(Err(e)) => {
1014 tracing::error!("endpoint_watcher: discovery stream error: {}; shutting down for discovery query: {:?}", e, discovery_query);
1015 break;
1016 }
1017 None => {
1018 break;
1019 }
1020 }
1021 }
1022 };
1023
1024 if let Some(discovery_source) = discovery_source_task.upgrade() {
1025 discovery_source.broadcast_event(&discovery_event);
1026 }
1027
1028 match discovery_event {
1029 DiscoveryEvent::Added(DiscoveryInstance::Endpoint(instance)) => {
1030 map.insert(instance.instance_id, instance);
1031 }
1032 DiscoveryEvent::Added(_) => {}
1033 DiscoveryEvent::ModelTaintsUpdated(_) => {}
1034 DiscoveryEvent::Resync(instances) => {
1035 map = instances
1036 .into_iter()
1037 .filter_map(|instance| {
1038 let DiscoveryInstance::Endpoint(instance) = instance else {
1039 return None;
1040 };
1041 Some((instance.instance_id, instance))
1042 })
1043 .collect();
1044 }
1045 DiscoveryEvent::Removed(id) => {
1046 if let DiscoveryInstanceId::Endpoint(endpoint_id) = id {
1047 map.remove(&endpoint_id.instance_id);
1048 }
1049 }
1050 }
1051
1052 let instances: Vec<Instance> = map.values().cloned().collect();
1053 if watch_tx.send(instances).is_err() {
1054 break;
1055 }
1056 }
1057 let _ = watch_tx.send(vec![]);
1058 });
1059
1060 Ok(discovery_source)
1061 }
1062}
1063
1064#[cfg(test)]
1065mod tests {
1066 use super::*;
1067 use crate::component::TransportType;
1068 use crate::discovery::{
1069 DiscoveryQuery, DiscoverySpec, DiscoveryStream, MockDiscovery, SharedMockRegistry,
1070 };
1071 use crate::{DistributedRuntime, Runtime, distributed::DistributedConfig};
1072 use futures::future::try_join_all;
1073
1074 struct ParkedProducerDiscovery {
1077 inner: MockDiscovery,
1078 producers: StdMutex<Vec<tokio::task::JoinHandle<()>>>,
1079 feeds: StdMutex<Vec<tokio::sync::mpsc::UnboundedSender<Result<DiscoveryEvent>>>>,
1080 }
1081
1082 #[async_trait::async_trait]
1083 impl Discovery for ParkedProducerDiscovery {
1084 fn instance_id(&self) -> u64 {
1085 self.inner.instance_id()
1086 }
1087
1088 async fn register_internal(&self, spec: DiscoverySpec) -> Result<DiscoveryInstance> {
1089 self.inner.register_internal(spec).await
1090 }
1091
1092 async fn unregister(&self, instance: DiscoveryInstance) -> Result<()> {
1093 self.inner.unregister(instance).await
1094 }
1095
1096 async fn list(&self, query: DiscoveryQuery) -> Result<Vec<DiscoveryInstance>> {
1097 self.inner.list(query).await
1098 }
1099
1100 async fn list_and_watch(
1101 &self,
1102 _query: DiscoveryQuery,
1103 cancel_token: Option<CancellationToken>,
1104 ) -> Result<DiscoveryStream> {
1105 let (feed_tx, mut feed) =
1106 tokio::sync::mpsc::unbounded_channel::<Result<DiscoveryEvent>>();
1107 self.feeds.lock().unwrap().push(feed_tx.clone());
1108 let producer = tokio::spawn(async move {
1109 let _feed_tx = feed_tx;
1110 match cancel_token {
1111 Some(token) => token.cancelled().await,
1112 None => std::future::pending::<()>().await,
1113 }
1114 });
1115 self.producers.lock().unwrap().push(producer);
1116
1117 Ok(Box::pin(async_stream::stream! {
1118 while let Some(event) = feed.recv().await {
1119 yield event;
1120 }
1121 }))
1122 }
1123 }
1124
1125 #[tokio::test]
1126 async fn test_instance_by_id_returns_owned_current_instance() {
1127 use crate::component::TransportType;
1128
1129 let rt = Runtime::from_current().unwrap();
1130 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1131 .await
1132 .unwrap();
1133 let endpoint = drt
1134 .namespace("test_instance_lookup".to_string())
1135 .unwrap()
1136 .component("test_component".to_string())
1137 .unwrap()
1138 .endpoint("test_endpoint".to_string());
1139 let mut client = endpoint.client().await.unwrap();
1140 let instances: Vec<_> = (1..=2)
1141 .map(|instance_id| Instance {
1142 namespace: "test_instance_lookup".to_string(),
1143 component: "test_component".to_string(),
1144 endpoint: "test_endpoint".to_string(),
1145 instance_id,
1146 transport: TransportType::Tcp(format!("127.0.0.1:{}", 9000 + instance_id)),
1147 device_type: None,
1148 request_plane_codec: None,
1149 })
1150 .collect();
1151 let (tx, rx) = tokio::sync::watch::channel(instances.clone());
1152 client.instance_source = Arc::new(rx);
1153
1154 assert_eq!(client.instance_by_id(1), Some(instances[0].clone()));
1155 let selected = client.instance_by_id(2).unwrap();
1156 assert_eq!(selected, instances[1]);
1157 assert!(client.instance_by_id(3).is_none());
1158
1159 let mut updated = selected.clone();
1160 updated.transport = TransportType::Nats("updated.subject".to_string());
1161 tx.send(vec![instances[0].clone(), updated.clone()])
1162 .unwrap();
1163 assert_eq!(client.instance_by_id(2), Some(updated));
1164
1165 tx.send(vec![instances[0].clone()]).unwrap();
1166 assert!(client.instance_by_id(2).is_none());
1167 assert_eq!(client.instance_by_id(1), Some(instances[0].clone()));
1168
1169 rt.shutdown();
1170 }
1171
1172 async fn wait_for_discovery_event(
1173 receiver: &mut DiscoveryEventReceiver,
1174 predicate: impl Fn(&DiscoveryEvent) -> bool,
1175 ) {
1176 tokio::time::timeout(Duration::from_secs(1), async {
1177 loop {
1178 let event = receiver.recv().await.expect("discovery event feed closed");
1179 if predicate(&event) {
1180 return;
1181 }
1182 }
1183 })
1184 .await
1185 .expect("expected discovery event was not received");
1186 }
1187
1188 async fn wait_for_watch_state<T>(
1189 receiver: &mut tokio::sync::watch::Receiver<Vec<T>>,
1190 predicate: impl Fn(&[T]) -> bool,
1191 ) {
1192 tokio::time::timeout(Duration::from_secs(1), async {
1193 loop {
1194 if predicate(receiver.borrow_and_update().as_slice()) {
1195 return;
1196 }
1197 receiver
1198 .changed()
1199 .await
1200 .expect("instance availability feed closed");
1201 }
1202 })
1203 .await
1204 .expect("expected instance availability state was not observed");
1205 }
1206
1207 #[test]
1208 fn test_inhibited_duration_from_env() {
1209 assert_eq!(
1210 inhibited_duration_from_env(|_| None),
1211 Duration::from_secs(DEFAULT_INHIBITED_DURATION_SECS)
1212 );
1213 assert_eq!(
1214 inhibited_duration_from_env(|_| Some("17".to_string())),
1215 Duration::from_secs(17)
1216 );
1217 assert_eq!(
1218 inhibited_duration_from_env(|_| Some("0".to_string())),
1219 Duration::ZERO
1220 );
1221 assert_eq!(
1222 inhibited_duration_from_env(|_| Some("invalid".to_string())),
1223 Duration::from_secs(DEFAULT_INHIBITED_DURATION_SECS)
1224 );
1225 }
1226
1227 #[tokio::test]
1228 async fn wait_for_routable_instances_waits_for_routing_publication() {
1229 let rt = Runtime::from_current().unwrap();
1230 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1231 .await
1232 .unwrap();
1233 let endpoint = drt
1234 .namespace("test_wait_routing".to_string())
1235 .unwrap()
1236 .component("backend".to_string())
1237 .unwrap()
1238 .endpoint("generate".to_string());
1239 let instance = Instance {
1240 namespace: "test_wait_routing".into(),
1241 component: "backend".into(),
1242 endpoint: "generate".into(),
1243 instance_id: 1,
1244 transport: TransportType::Nats("test.subject".into()),
1245 device_type: None,
1246 request_plane_codec: None,
1247 };
1248 let (source_tx, source_rx) = tokio::sync::watch::channel(vec![instance.clone()]);
1249 let source =
1250 EndpointDiscoverySource::new(source_rx.clone(), CancellationToken::new().drop_guard());
1251 let (routing, available) = RoutingInstancesState::new(vec![]);
1252 let client = Client {
1253 endpoint,
1254 endpoint_discovery_source: Arc::new(source),
1255 instance_source: Arc::new(source_rx),
1256 routing_instances: Arc::new(routing),
1257 instance_avail_owner: Arc::new(available),
1258 reconcile_interval: Duration::ZERO,
1259 };
1260
1261 assert_eq!(
1263 client.wait_for_instances().await.unwrap(),
1264 vec![instance.clone()]
1265 );
1266 let ready = client.wait_for_routable_instances();
1267 tokio::pin!(ready);
1268 assert!(futures::poll!(ready.as_mut()).is_pending());
1269 client.routing_instances.reconcile_discovered(vec![1]);
1270 assert_eq!(ready.await.unwrap(), vec![instance]);
1271 assert_eq!(client.instance_ids_avail(), vec![1]);
1272
1273 client.routing_instances.reconcile_discovered(vec![]);
1274 drop(source_tx);
1275 assert!(client.wait_for_routable_instances().await.is_err());
1276 rt.shutdown();
1277 }
1278
1279 #[tokio::test]
1280 async fn concurrent_clients_share_one_discovery_source() {
1281 let rt = Runtime::from_current().unwrap();
1282 let drt = DistributedRuntime::new(rt, DistributedConfig::process_local())
1283 .await
1284 .unwrap();
1285 let endpoint = drt
1286 .namespace("test_shared_discovery_source".to_string())
1287 .unwrap()
1288 .component("test_component".to_string())
1289 .unwrap()
1290 .endpoint("decode".to_string());
1291
1292 let clients = try_join_all((0..8).map(|_| endpoint.client()))
1295 .await
1296 .unwrap();
1297
1298 let shared = &clients[0].endpoint_discovery_source;
1299 for (index, client) in clients.iter().enumerate() {
1300 assert!(
1301 Arc::ptr_eq(shared, &client.endpoint_discovery_source),
1302 "client {index} watches the endpoint through a second discovery source"
1303 );
1304 }
1305 }
1306
1307 #[tokio::test]
1308 async fn dropping_a_discovery_source_stops_its_backend_watch() {
1309 let rt = Runtime::from_current().unwrap();
1310 let drt = DistributedRuntime::new(rt, DistributedConfig::process_local())
1311 .await
1312 .unwrap();
1313 let endpoint = drt
1314 .namespace("test_discovery_source_drop".to_string())
1315 .unwrap()
1316 .component("test_component".to_string())
1317 .unwrap()
1318 .endpoint("decode".to_string());
1319
1320 let discovery = ParkedProducerDiscovery {
1321 inner: MockDiscovery::new(Some(1), SharedMockRegistry::new()),
1322 producers: StdMutex::default(),
1323 feeds: StdMutex::default(),
1324 };
1325
1326 let source = Client::spawn_dynamic_discovery_source(
1327 &endpoint,
1328 &discovery,
1329 drt.primary_token().child_token(),
1330 )
1331 .await
1332 .unwrap();
1333 let producer = discovery
1334 .producers
1335 .lock()
1336 .unwrap()
1337 .pop()
1338 .expect("the source established no backend watch");
1339 tokio::task::yield_now().await;
1340 assert!(
1341 !producer.is_finished(),
1342 "a live source must keep watching the endpoint"
1343 );
1344
1345 drop(source);
1346 tokio::time::timeout(Duration::from_secs(1), producer)
1347 .await
1348 .expect("the backend watch outlives the source that established it")
1349 .unwrap();
1350 }
1351
1352 fn endpoint_instance(endpoint: &Endpoint, instance_id: u64) -> Instance {
1353 Instance {
1354 namespace: endpoint.component.namespace.name.clone(),
1355 component: endpoint.component.name.clone(),
1356 endpoint: endpoint.name.clone(),
1357 instance_id,
1358 transport: TransportType::Tcp(format!("127.0.0.1:{}", 8000 + instance_id)),
1359 device_type: None,
1360 request_plane_codec: None,
1361 }
1362 }
1363
1364 #[tokio::test]
1365 async fn a_resync_replaces_the_instances_of_a_discovery_source() {
1366 let rt = Runtime::from_current().unwrap();
1367 let drt = DistributedRuntime::new(rt, DistributedConfig::process_local())
1368 .await
1369 .unwrap();
1370 let endpoint = drt
1371 .namespace("test_discovery_source_resync".to_string())
1372 .unwrap()
1373 .component("test_component".to_string())
1374 .unwrap()
1375 .endpoint("decode".to_string());
1376 let discovery = ParkedProducerDiscovery {
1377 inner: MockDiscovery::new(Some(1), SharedMockRegistry::new()),
1378 producers: StdMutex::default(),
1379 feeds: StdMutex::default(),
1380 };
1381
1382 let source = Client::spawn_dynamic_discovery_source(
1383 &endpoint,
1384 &discovery,
1385 drt.primary_token().child_token(),
1386 )
1387 .await
1388 .unwrap();
1389 let feed = discovery
1390 .feeds
1391 .lock()
1392 .unwrap()
1393 .pop()
1394 .expect("the source established no backend watch");
1395 let mut instances = source.instance_receiver();
1396
1397 let first = endpoint_instance(&endpoint, 1);
1398 feed.send(Ok(DiscoveryEvent::Added(DiscoveryInstance::Endpoint(
1399 first.clone(),
1400 ))))
1401 .unwrap();
1402 wait_for_watch_state(&mut instances, |instances| {
1403 instances == std::slice::from_ref(&first)
1404 })
1405 .await;
1406
1407 let second = endpoint_instance(&endpoint, 2);
1408 feed.send(Ok(DiscoveryEvent::Resync(vec![
1409 DiscoveryInstance::Endpoint(second.clone()),
1410 ])))
1411 .unwrap();
1412 wait_for_watch_state(&mut instances, |instances| {
1413 instances == std::slice::from_ref(&second)
1414 })
1415 .await;
1416 }
1417
1418 #[tokio::test]
1419 async fn dropping_last_client_releases_routing_state() {
1420 let rt = Runtime::from_current().unwrap();
1421 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1422 .await
1423 .unwrap();
1424 let endpoint = drt
1425 .namespace("test_client_lifecycle".to_string())
1426 .unwrap()
1427 .component("test_component".to_string())
1428 .unwrap()
1429 .endpoint("decode".to_string());
1430 let client = endpoint.client().await.unwrap();
1431 let routing_instances = Arc::downgrade(&client.routing_instances);
1432 let discovery_source = Arc::downgrade(&client.endpoint_discovery_source);
1433 let mut raw_watcher = client.instance_source.as_ref().clone();
1434 let mut watcher = client.instance_avail_watcher();
1435 let mut event_watcher = client.subscribe_discovery_events();
1436 raw_watcher.borrow_and_update();
1437 watcher.borrow_and_update();
1438
1439 drop(client);
1440 assert!(routing_instances.upgrade().is_some());
1441 assert!(discovery_source.upgrade().is_some());
1442
1443 endpoint.register_endpoint_instance().await.unwrap();
1444 wait_for_watch_state(&mut watcher, |instances| instances.len() == 1).await;
1445 wait_for_discovery_event(&mut event_watcher, |event| {
1446 matches!(event, DiscoveryEvent::Added(DiscoveryInstance::Endpoint(_)))
1447 })
1448 .await;
1449
1450 endpoint.unregister_endpoint_instance().await.unwrap();
1451 wait_for_watch_state(&mut watcher, |instances| instances.is_empty()).await;
1452 assert!(raw_watcher.borrow_and_update().is_empty());
1453 wait_for_discovery_event(&mut event_watcher, |event| {
1454 matches!(event, DiscoveryEvent::Removed(_))
1455 })
1456 .await;
1457
1458 drop(watcher);
1459
1460 tokio::time::timeout(Duration::from_secs(1), async {
1461 while routing_instances.strong_count() != 0 {
1462 tokio::task::yield_now().await;
1463 }
1464 })
1465 .await
1466 .expect("client monitor retained state after its last observer was dropped");
1467 assert!(discovery_source.upgrade().is_some());
1468
1469 endpoint.register_endpoint_instance().await.unwrap();
1470 wait_for_watch_state(&mut raw_watcher, |instances| instances.len() == 1).await;
1471 wait_for_discovery_event(&mut event_watcher, |event| {
1472 matches!(event, DiscoveryEvent::Added(DiscoveryInstance::Endpoint(_)))
1473 })
1474 .await;
1475 endpoint.unregister_endpoint_instance().await.unwrap();
1476 wait_for_watch_state(&mut raw_watcher, |instances| instances.is_empty()).await;
1477 wait_for_discovery_event(&mut event_watcher, |event| {
1478 matches!(event, DiscoveryEvent::Removed(_))
1479 })
1480 .await;
1481
1482 drop(event_watcher);
1483 tokio::time::timeout(Duration::from_secs(1), async {
1484 while discovery_source.strong_count() != 0 {
1485 tokio::task::yield_now().await;
1486 }
1487 })
1488 .await
1489 .expect("discovery event receiver retained its source after being dropped");
1490
1491 rt.shutdown();
1492 }
1493
1494 #[tokio::test]
1497 async fn test_instance_reconciliation() {
1498 const TEST_RECONCILE_INTERVAL: Duration = Duration::from_millis(100);
1499
1500 let rt = Runtime::from_current().unwrap();
1501 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1503 .await
1504 .unwrap();
1505 let ns = drt.namespace("test_reconciliation".to_string()).unwrap();
1506 let component = ns.component("test_component".to_string()).unwrap();
1507 let endpoint = component.endpoint("test_endpoint".to_string());
1508
1509 let client = Client::with_reconcile_interval(endpoint, TEST_RECONCILE_INTERVAL)
1511 .await
1512 .unwrap();
1513
1514 assert!(client.instance_ids_avail().is_empty());
1516
1517 client.override_instance_avail(vec![1, 2, 3]);
1520
1521 assert_eq!(client.instance_ids_avail(), vec![1u64, 2, 3]);
1522
1523 client.report_instance_down(2);
1525 assert_eq!(client.instance_ids_avail(), vec![1u64, 3]);
1526
1527 tokio::time::sleep(TEST_RECONCILE_INTERVAL + Duration::from_millis(50)).await;
1531
1532 assert!(
1534 client.instance_ids_avail().is_empty(),
1535 "After reconciliation, instance_avail should match instance_source"
1536 );
1537
1538 rt.shutdown();
1539 }
1540
1541 #[tokio::test]
1543 async fn test_zero_inhibited_duration_leaves_instance_routable() {
1544 let rt = Runtime::from_current().unwrap();
1545 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1546 .await
1547 .unwrap();
1548 let ns = drt
1549 .namespace("test_disabled_inhibition".to_string())
1550 .unwrap();
1551 let component = ns.component("test_component".to_string()).unwrap();
1552 let endpoint = component.endpoint("test_endpoint".to_string());
1553
1554 let client = Client::with_reconcile_interval(endpoint, Duration::ZERO)
1555 .await
1556 .unwrap();
1557
1558 client.override_instance_avail(vec![1, 2, 3]);
1559 client.report_instance_down(2);
1560
1561 assert_eq!(
1562 client.instance_ids_avail(),
1563 vec![1, 2, 3],
1564 "a zero inhibited duration should leave the reported instance routable"
1565 );
1566
1567 rt.shutdown();
1568 }
1569
1570 #[tokio::test]
1572 async fn test_report_instance_down() {
1573 let rt = Runtime::from_current().unwrap();
1574 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1576 .await
1577 .unwrap();
1578 let ns = drt.namespace("test_report_down".to_string()).unwrap();
1579 let component = ns.component("test_component".to_string()).unwrap();
1580 let endpoint = component.endpoint("test_endpoint".to_string());
1581
1582 let client = endpoint.client().await.unwrap();
1583
1584 client.override_instance_avail(vec![1, 2, 3]);
1586 assert_eq!(client.instance_ids_avail(), vec![1u64, 2, 3]);
1587
1588 client.report_instance_down(2);
1590
1591 let avail = client.instance_ids_avail();
1593 assert!(avail.contains(&1), "Instance 1 should still be available");
1594 assert!(
1595 !avail.contains(&2),
1596 "Instance 2 should be removed after report_instance_down"
1597 );
1598 assert!(avail.contains(&3), "Instance 3 should still be available");
1599
1600 rt.shutdown();
1601 }
1602
1603 #[tokio::test]
1604 async fn test_overloaded_instance_ids_returns_none_when_empty() {
1605 let rt = Runtime::from_current().unwrap();
1606 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1607 .await
1608 .unwrap();
1609 let ns = drt.namespace("test_overloaded_ids".to_string()).unwrap();
1610 let component = ns.component("test_component".to_string()).unwrap();
1611 let endpoint = component.endpoint("test_endpoint".to_string());
1612 let client = endpoint.client().await.unwrap();
1613
1614 assert_eq!(client.overloaded_instance_ids(), None);
1615
1616 assert!(client.set_overloaded_instances(&[7]));
1617 assert_eq!(client.overloaded_instance_ids(), Some(HashSet::from([7])));
1618 assert!(!client.set_overloaded_instances(&[7]));
1619
1620 assert!(client.set_overloaded_instances(&[]));
1621 assert_eq!(client.overloaded_instance_ids(), None);
1622 assert!(!client.set_overloaded_instances(&[]));
1623
1624 rt.shutdown();
1625 }
1626
1627 #[tokio::test(start_paused = true)]
1628 async fn request_path_overload_expires_without_monitor_update() {
1629 let rt = Runtime::from_current().unwrap();
1630 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1631 .await
1632 .unwrap();
1633 let client = drt
1634 .namespace("test_request_overload_expiry".to_string())
1635 .unwrap()
1636 .component("test_component".to_string())
1637 .unwrap()
1638 .endpoint("decode".to_string())
1639 .client()
1640 .await
1641 .unwrap();
1642
1643 client.mark_overloaded_immediate(7);
1644 assert_eq!(client.overloaded_instance_ids(), Some(HashSet::from([7])));
1645
1646 tokio::time::advance(REQUEST_PATH_OVERLOAD_COOLDOWN).await;
1647
1648 assert_eq!(client.overloaded_instance_ids(), None);
1649 assert!(!client.overload_reconciliation_needed());
1650 rt.shutdown();
1651 }
1652
1653 #[tokio::test]
1654 async fn request_path_expiry_preserves_monitor_overload() {
1655 let rt = Runtime::from_current().unwrap();
1656 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1657 .await
1658 .unwrap();
1659 let client = drt
1660 .namespace("test_request_expiry_preserves_monitor".to_string())
1661 .unwrap()
1662 .component("test_component".to_string())
1663 .unwrap()
1664 .endpoint("decode".to_string())
1665 .client()
1666 .await
1667 .unwrap();
1668 let now = Instant::now();
1669
1670 assert!(client.set_overloaded_instances(&[11]));
1671 client
1672 .routing_instances
1673 .mark_overloaded_until(7, now + Duration::from_secs(1));
1674 client
1675 .routing_instances
1676 .mark_overloaded_until(8, now + Duration::from_secs(2));
1677
1678 client
1679 .routing_instances
1680 .reconcile_expired_request_overloads(now + Duration::from_millis(1500));
1681
1682 assert_eq!(
1683 client.overloaded_instance_ids(),
1684 Some(HashSet::from([8, 11]))
1685 );
1686 assert!(client.overload_reconciliation_needed());
1687 rt.shutdown();
1688 }
1689
1690 #[tokio::test]
1691 async fn test_instance_reconciliation_preserves_overloaded_existing_instances() {
1692 const TEST_RECONCILE_INTERVAL: Duration = Duration::from_millis(50);
1693
1694 let rt = Runtime::from_current().unwrap();
1695 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1696 .await
1697 .unwrap();
1698 let ns = drt
1699 .namespace("test_overloaded_reconciliation".to_string())
1700 .unwrap();
1701 let component = ns.component("test_component".to_string()).unwrap();
1702 let endpoint = component.endpoint("test_endpoint".to_string());
1703
1704 let client = Client::with_reconcile_interval(endpoint.clone(), TEST_RECONCILE_INTERVAL)
1705 .await
1706 .unwrap();
1707 endpoint.register_endpoint_instance().await.unwrap();
1708 let instances = client.wait_for_instances().await.unwrap();
1709 let worker_id = instances[0].id();
1710
1711 for _ in 0..10 {
1712 if client.instance_ids_free().contains(&worker_id) {
1713 break;
1714 }
1715 tokio::time::sleep(TEST_RECONCILE_INTERVAL).await;
1716 }
1717 assert!(
1718 client.instance_ids_free().contains(&worker_id),
1719 "worker should be free after initial discovery reconciliation"
1720 );
1721
1722 client.set_overloaded_instances(&[worker_id]);
1723 assert!(
1724 client.instance_ids_free().is_empty(),
1725 "worker should be overloaded before periodic reconciliation"
1726 );
1727
1728 tokio::time::sleep(TEST_RECONCILE_INTERVAL + Duration::from_millis(50)).await;
1729
1730 assert!(
1731 client.instance_ids_free().is_empty(),
1732 "periodic reconciliation should not mark an existing overloaded worker free"
1733 );
1734
1735 rt.shutdown();
1736 }
1737
1738 #[tokio::test]
1739 async fn test_report_instance_down_preserves_overloaded_state() {
1740 const TEST_RECONCILE_INTERVAL: Duration = Duration::from_millis(50);
1741
1742 let rt = Runtime::from_current().unwrap();
1743 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1744 .await
1745 .unwrap();
1746 let ns = drt
1747 .namespace("test_report_down_preserves_overloaded".to_string())
1748 .unwrap();
1749 let component = ns.component("test_component".to_string()).unwrap();
1750 let endpoint = component.endpoint("test_endpoint".to_string());
1751
1752 let client = Client::with_reconcile_interval(endpoint.clone(), TEST_RECONCILE_INTERVAL)
1753 .await
1754 .unwrap();
1755 endpoint.register_endpoint_instance().await.unwrap();
1756 let instances = client.wait_for_instances().await.unwrap();
1757 let worker_id = instances[0].id();
1758
1759 for _ in 0..10 {
1760 if client.instance_ids_avail().contains(&worker_id) {
1761 break;
1762 }
1763 tokio::time::sleep(TEST_RECONCILE_INTERVAL).await;
1764 }
1765
1766 client.set_overloaded_instances(&[worker_id]);
1767 client.report_instance_down(worker_id);
1768
1769 assert!(
1770 !client.instance_ids_avail().contains(&worker_id),
1771 "reported-down worker should leave routable availability"
1772 );
1773 assert_eq!(
1774 client.routing_instance_counts().overloaded,
1775 1,
1776 "reported-down worker should remain overloaded while still discovered"
1777 );
1778 assert!(
1779 client.instance_ids_free().is_empty(),
1780 "reported-down overloaded worker should not become free"
1781 );
1782
1783 endpoint.unregister_endpoint_instance().await.unwrap();
1784 for _ in 0..10 {
1785 if client.routing_instance_counts().overloaded == 0 {
1786 break;
1787 }
1788 tokio::time::sleep(TEST_RECONCILE_INTERVAL).await;
1789 }
1790
1791 assert_eq!(
1792 client.routing_instance_counts().overloaded,
1793 0,
1794 "stable discovery removal should clear overloaded state"
1795 );
1796
1797 rt.shutdown();
1798 }
1799
1800 #[tokio::test]
1801 async fn test_instance_reconciliation_prunes_removed_overloaded_instances() {
1802 const TEST_RECONCILE_INTERVAL: Duration = Duration::from_millis(50);
1803
1804 let rt = Runtime::from_current().unwrap();
1805 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1806 .await
1807 .unwrap();
1808 let ns = drt
1809 .namespace("test_removed_overloaded_cleanup".to_string())
1810 .unwrap();
1811 let component = ns.component("test_component".to_string()).unwrap();
1812 let endpoint = component.endpoint("test_endpoint".to_string());
1813
1814 let client = Client::with_reconcile_interval(endpoint.clone(), TEST_RECONCILE_INTERVAL)
1815 .await
1816 .unwrap();
1817 endpoint.register_endpoint_instance().await.unwrap();
1818 let instances = client.wait_for_instances().await.unwrap();
1819 let worker_id = instances[0].id();
1820
1821 client.set_overloaded_instances(&[worker_id]);
1822 assert_eq!(client.routing_instance_counts().overloaded, 1);
1823 assert!(client.instance_ids_free().is_empty());
1824
1825 endpoint.unregister_endpoint_instance().await.unwrap();
1826 for _ in 0..10 {
1827 if client.routing_instance_counts().overloaded == 0 {
1828 break;
1829 }
1830 tokio::time::sleep(TEST_RECONCILE_INTERVAL).await;
1831 }
1832
1833 assert_eq!(
1834 client.routing_instance_counts().overloaded,
1835 0,
1836 "removed discovered workers should not remain in overloaded state"
1837 );
1838
1839 rt.shutdown();
1840 }
1841
1842 #[tokio::test]
1843 async fn test_instance_ids_free_excludes_overloaded_new_instances() {
1844 const TEST_RECONCILE_INTERVAL: Duration = Duration::from_millis(50);
1845
1846 let rt = Runtime::from_current().unwrap();
1847 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1848 .await
1849 .unwrap();
1850 let worker_id = drt.connection_id();
1851 let ns = drt
1852 .namespace("test_new_overloaded_reconciliation".to_string())
1853 .unwrap();
1854 let component = ns.component("test_component".to_string()).unwrap();
1855 let endpoint = component.endpoint("test_endpoint".to_string());
1856
1857 let client = Client::with_reconcile_interval(endpoint.clone(), TEST_RECONCILE_INTERVAL)
1858 .await
1859 .unwrap();
1860 client.set_overloaded_instances(&[worker_id]);
1861
1862 endpoint.register_endpoint_instance().await.unwrap();
1863 let instances = client.wait_for_instances().await.unwrap();
1864 assert_eq!(instances[0].id(), worker_id);
1865 assert!(
1866 client.instance_ids_free().is_empty(),
1867 "newly discovered overloaded worker should not be free"
1868 );
1869
1870 tokio::time::sleep(TEST_RECONCILE_INTERVAL + Duration::from_millis(50)).await;
1871
1872 assert!(
1873 client.instance_ids_free().is_empty(),
1874 "discovery reconciliation should not affect recomputed free workers"
1875 );
1876
1877 rt.shutdown();
1878 }
1879
1880 #[tokio::test]
1881 async fn test_discovery_add_updates_free_without_overloaded_publish() {
1882 const TEST_RECONCILE_INTERVAL: Duration = Duration::from_millis(50);
1883
1884 let rt = Runtime::from_current().unwrap();
1885 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1886 .await
1887 .unwrap();
1888 let ns = drt
1889 .namespace("test_free_updates_on_discovery_add".to_string())
1890 .unwrap();
1891 let component = ns.component("test_component".to_string()).unwrap();
1892 let endpoint = component.endpoint("test_endpoint".to_string());
1893
1894 let client = Client::with_reconcile_interval(endpoint.clone(), TEST_RECONCILE_INTERVAL)
1895 .await
1896 .unwrap();
1897 endpoint.register_endpoint_instance().await.unwrap();
1898 let instances = client.wait_for_instances().await.unwrap();
1899 let worker_id = instances[0].id();
1900
1901 for _ in 0..10 {
1902 if client.instance_ids_free().contains(&worker_id) {
1903 break;
1904 }
1905 tokio::time::sleep(TEST_RECONCILE_INTERVAL).await;
1906 }
1907
1908 assert_eq!(
1909 client.instance_ids_free(),
1910 vec![worker_id],
1911 "newly discovered non-overloaded workers should appear free without an overload update"
1912 );
1913
1914 rt.shutdown();
1915 }
1916
1917 #[tokio::test]
1919 async fn test_instance_avail_watcher() {
1920 let rt = Runtime::from_current().unwrap();
1921 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1923 .await
1924 .unwrap();
1925 let ns = drt.namespace("test_watcher".to_string()).unwrap();
1926 let component = ns.component("test_component".to_string()).unwrap();
1927 let endpoint = component.endpoint("test_endpoint".to_string());
1928
1929 let client = endpoint.client().await.unwrap();
1930 let watcher = client.instance_avail_watcher();
1931
1932 client.override_instance_avail(vec![1, 2, 3]);
1934
1935 client.report_instance_down(2);
1937
1938 let current = watcher.borrow().clone();
1941 assert_eq!(current, vec![1, 3]);
1942
1943 rt.shutdown();
1944 }
1945
1946 #[tokio::test]
1960 async fn monitor_instance_source_exits_on_its_own_cancellation_token() {
1961 let rt = Runtime::from_current().unwrap();
1962 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
1963 .await
1964 .unwrap();
1965 let ns = drt
1966 .namespace("test_monitor_instance_source_cancellation".to_string())
1967 .unwrap();
1968 let component = ns.component("test_component".to_string()).unwrap();
1969 let endpoint = component.endpoint("test_endpoint".to_string());
1970
1971 let cancel_token = tokio_util::sync::CancellationToken::new();
1972 let client = Client::with_cancellation(endpoint.clone(), cancel_token.clone())
1973 .await
1974 .unwrap();
1975
1976 tokio::time::sleep(Duration::from_millis(200)).await;
1979 assert!(
1980 Arc::strong_count(&client.routing_instances) > 1,
1981 "monitor task must be running (and holding its capture) before cancellation"
1982 );
1983
1984 cancel_token.cancel();
1985 tokio::time::timeout(Duration::from_secs(5), async {
1986 while Arc::strong_count(&client.routing_instances) > 1 {
1987 tokio::time::sleep(Duration::from_millis(10)).await;
1988 }
1989 })
1990 .await
1991 .expect("monitor_instance_source task must exit when its cancel_token cancels");
1992
1993 rt.shutdown();
1994 }
1995
1996 #[tokio::test]
1997 async fn admitted_client_never_routes_unadmitted_endpoint_instances() {
1998 let rt = Runtime::from_current().unwrap();
1999 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2000 .await
2001 .unwrap();
2002 let ns = drt.namespace("test_admitted_client".to_string()).unwrap();
2003 let component = ns.component("test_component".to_string()).unwrap();
2004 let endpoint = component.endpoint("test_endpoint".to_string());
2005 let endpoint_client = endpoint.client().await.unwrap();
2006 endpoint.register_endpoint_instance().await.unwrap();
2007 let worker_id = endpoint_client.wait_for_instances().await.unwrap()[0].id();
2008
2009 let (admission_tx, admission_rx) = tokio::sync::watch::channel(Vec::new());
2010 let admitted_client = endpoint_client.with_admitted_instances(admission_rx);
2011 let mut admitted = admitted_client.instance_avail_watcher();
2012 assert!(admitted.borrow().is_empty());
2013
2014 admission_tx.send_replace(vec![worker_id]);
2015 tokio::time::timeout(Duration::from_secs(1), admitted.changed())
2016 .await
2017 .unwrap()
2018 .unwrap();
2019 assert_eq!(admitted.borrow_and_update().clone(), vec![worker_id]);
2020
2021 admission_tx.send_replace(Vec::new());
2022 tokio::time::timeout(Duration::from_secs(1), admitted.changed())
2023 .await
2024 .unwrap()
2025 .unwrap();
2026 assert!(admitted.borrow_and_update().is_empty());
2027
2028 rt.shutdown();
2029 }
2030
2031 #[tokio::test]
2033 async fn test_concurrent_select_and_increment() {
2034 let state = Arc::new(RoutingOccupancyState::default());
2035 let instance_ids: Vec<u64> = vec![100, 200, 300];
2036 let num_requests = 90;
2037
2038 let mut handles = Vec::new();
2039 for _ in 0..num_requests {
2040 let state = state.clone();
2041 let ids = instance_ids.clone();
2042 handles.push(tokio::spawn(async move {
2043 state.select_exact_min_and_increment(&ids).await
2044 }));
2045 }
2046
2047 for handle in handles {
2048 handle.await.unwrap();
2049 }
2050
2051 assert_eq!(state.load(100), 30);
2052 assert_eq!(state.load(200), 30);
2053 assert_eq!(state.load(300), 30);
2054 }
2055
2056 #[tokio::test]
2057 async fn test_select_exact_min_and_increment_randomizes_ties() {
2058 let mut selected = [false; 3];
2059
2060 for _ in 0..120 {
2061 let state = RoutingOccupancyState::default();
2062 let picked = state
2063 .select_exact_min_and_increment(&[10, 20, 30])
2064 .await
2065 .unwrap();
2066 match picked {
2067 10 => selected[0] = true,
2068 20 => selected[1] = true,
2069 30 => selected[2] = true,
2070 _ => panic!("unexpected worker id: {picked}"),
2071 }
2072 }
2073
2074 let selected_count = selected.into_iter().filter(|seen| *seen).count();
2075 assert!(
2076 selected_count > 1,
2077 "tie-breaking should not always select the first minimum-load worker"
2078 );
2079 }
2080
2081 #[tokio::test]
2082 async fn test_connection_counts() {
2083 let rt = Runtime::from_current().unwrap();
2084 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2085 .await
2086 .unwrap();
2087 let ns = drt.namespace("test_ll_counts".to_string()).unwrap();
2088 let component = ns.component("test_component".to_string()).unwrap();
2089 let endpoint = component.endpoint("test_endpoint".to_string());
2090
2091 let state1 = get_or_create_routing_occupancy_state(&endpoint).await;
2092 let state2 = get_or_create_routing_occupancy_state(&endpoint).await;
2093
2094 let picked1 = state1
2095 .select_exact_min_and_increment(&[10, 20, 30])
2096 .await
2097 .unwrap();
2098 assert_eq!(state1.load(picked1), 1);
2099
2100 let picked2 = state1
2101 .select_exact_min_and_increment(&[10, 20, 30])
2102 .await
2103 .unwrap();
2104 assert_ne!(picked1, picked2);
2105
2106 assert_eq!(state2.load(10), state1.load(10));
2108 assert_eq!(state2.load(20), state1.load(20));
2109 assert_eq!(state2.load(30), state1.load(30));
2110
2111 state2.decrement(picked1);
2112 assert_eq!(state1.load(picked1), if picked1 == picked2 { 1 } else { 0 });
2113
2114 rt.shutdown();
2115 }
2116
2117 #[tokio::test]
2118 async fn test_least_loaded_state_retain_preserves_live_counts() {
2119 let state = RoutingOccupancyState::default();
2120
2121 state.select_exact_min_and_increment(&[1, 2, 3]).await;
2123 state.select_exact_min_and_increment(&[1, 2, 3]).await;
2124 state.select_exact_min_and_increment(&[1, 2, 3]).await;
2125 assert_eq!(state.load(1), 1);
2127 assert_eq!(state.load(2), 1);
2128 assert_eq!(state.load(3), 1);
2129
2130 state.retain(&[1, 3]);
2132
2133 assert_eq!(state.load(1), 1);
2134 assert_eq!(state.load(2), 1);
2135 assert_eq!(state.load(3), 1);
2136 }
2137
2138 #[tokio::test]
2139 async fn test_monitor_instance_source_defers_removed_worker_cleanup() {
2140 const TEST_RECONCILE_INTERVAL: Duration = Duration::from_millis(50);
2141
2142 let rt = Runtime::from_current().unwrap();
2143 let drt = DistributedRuntime::new(rt.clone(), DistributedConfig::process_local())
2144 .await
2145 .unwrap();
2146 let ns = drt.namespace("test_occupancy_cleanup".to_string()).unwrap();
2147 let component = ns.component("test_component".to_string()).unwrap();
2148 let endpoint = component.endpoint("test_endpoint".to_string());
2149
2150 let client = Client::with_reconcile_interval(endpoint.clone(), TEST_RECONCILE_INTERVAL)
2151 .await
2152 .unwrap();
2153 endpoint.register_endpoint_instance().await.unwrap();
2154 client.wait_for_instances().await.unwrap();
2155
2156 let worker_id = client.instance_ids_avail()[0];
2157 let state = get_or_create_routing_occupancy_state(&endpoint).await;
2158 state.increment(worker_id);
2159 assert_eq!(state.load(worker_id), 1);
2160
2161 endpoint.unregister_endpoint_instance().await.unwrap();
2162
2163 for _ in 0..10 {
2164 if !client.instance_ids().contains(&worker_id) {
2165 break;
2166 }
2167 tokio::time::sleep(TEST_RECONCILE_INTERVAL).await;
2168 }
2169
2170 assert_eq!(
2171 state.load(worker_id),
2172 1,
2173 "discovery absence must retain live accounting"
2174 );
2175 state.decrement(worker_id);
2176 assert_eq!(state.load(worker_id), 0);
2177
2178 rt.shutdown();
2179 }
2180}