Skip to main content

dynamo_runtime/component/
client.rs

1// SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4use 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
22/// Default interval for periodic reconciliation of instance_avail with instance_source
23const DEFAULT_INHIBITED_DURATION_SECS: u64 = 5;
24
25/// Request-path overload is a short-lived routing hint. A worker is probed
26/// again after this cooldown if no authoritative load update arrives first.
27const REQUEST_PATH_OVERLOAD_COOLDOWN: Duration = Duration::from_secs(1);
28
29/// Process-wide inhibited duration, resolved from the environment on first client construction.
30static 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/// Shared endpoint discovery state for a single endpoint query.
54///
55/// This wraps both the coalesced instance snapshot used for routing decisions
56/// and a raw, lossless per-subscriber event feed used by the response-stream
57/// cancellation watcher. Both outputs are driven by a single underlying
58/// discovery `list_and_watch` task so clients do not multiply control-plane
59/// watches.
60///
61/// Dropping the source stops that watch. A backend producer can park on its own feed without
62/// noticing that its consumer is gone, as the Kubernetes watcher does, so the token ends it.
63#[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    /// IDs not currently reported overloaded, derived from `discovered - overloaded`.
123    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    /// True after this client has observed at least one discovered instance.
137    /// Once set, a later empty snapshot is authoritative rather than startup
138    /// absence of information.
139    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        // Route through from_parts so `free_ids` is recomputed from the new
261        // routable set instead of carrying the stale value forward.
262        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    /// Add a single instance to the overloaded set (immediate
282    /// backpressure mark). A monitor update clears all request-path
283    /// marks. Otherwise, each mark expires at its own deadline.
284    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(&current));
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    // This is me
492    pub endpoint: Endpoint,
493    // Shared endpoint discovery source backing both snapshots and raw events.
494    endpoint_discovery_source: Arc<EndpointDiscoverySource>,
495    // These are the remotes I know about from watching key-value store
496    pub instance_source: Arc<tokio::sync::watch::Receiver<Vec<Instance>>>,
497    // Immutable routing snapshot. Free IDs are derived from discovered IDs and overloaded IDs.
498    routing_instances: Arc<RoutingInstancesState>,
499    // Client clones and standalone watchers jointly own the reconciliation task.
500    instance_avail_owner: Arc<tokio::sync::watch::Receiver<Vec<u64>>>,
501    /// Interval for periodic reconciliation of instance_avail with instance_source.
502    /// This ensures instances removed via `report_instance_down` are eventually restored.
503    /// A zero value disables local worker inhibition.
504    reconcile_interval: Duration,
505}
506
507impl Client {
508    // Client with auto-discover instances using key-value store
509    pub(crate) async fn new(endpoint: Endpoint) -> Result<Self> {
510        Self::with_reconcile_interval(endpoint, *INHIBITED_DURATION).await
511    }
512
513    /// Like [`Self::new`], but the `monitor_instance_source` background task
514    /// is bound to `cancel_token` instead of the process-wide primary token.
515    /// See [`Self::with_reconcile_interval_and_cancellation`] for why a
516    /// caller whose own scope is narrower than the process needs this.
517    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    /// Create a client with a custom reconcile interval.
526    /// The reconcile interval controls how often `instance_avail` is reset to match
527    /// `instance_source`, restoring any instances removed via `report_instance_down`.
528    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    /// Like [`Self::with_reconcile_interval`], but the `monitor_instance_source`
538    /// background task is bound to `cancel_token` rather than the process-wide
539    /// primary token.
540    ///
541    /// A caller that builds a `Client` scoped to something narrower than the
542    /// process — a monitor bound to one `WorkerSet`'s lifecycle, say — must use
543    /// this constructor. `Client` is `Clone`, and `monitor_instance_source`
544    /// captures its own clone before returning, so dropping every `Client`
545    /// handle the caller holds does not stop that task; only cancelling its
546    /// token does. Built through [`Self::new`] or [`Self::with_reconcile_interval`]
547    /// instead, that task runs until process shutdown regardless of how long
548    /// the caller actually keeps the `Client` around.
549    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        // Seed instance_avail from the current instance_source snapshot so that
563        // callers who proceed immediately after wait_for_instances (which reads
564        // instance_source directly) will also find instances in instance_avail
565        // (which is read by the routing methods like random/round_robin).
566        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    /// Instances available from watching key-value store
585    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    /// Whether the live discovery source (not the asynchronously reconciled
602    /// routing snapshot) currently contains this instance.
603    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    /// Whether the latest discovery snapshot contains this instance, including inhibited workers.
611    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    /// Routable instance ids excluding those currently flagged overloaded — the set used
624    /// for load-aware (random / round-robin) worker selection.
625    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    /// Get a watcher for available instance IDs
638    pub fn instance_avail_watcher(&self) -> tokio::sync::watch::Receiver<Vec<u64>> {
639        self.instance_avail_owner.as_ref().clone()
640    }
641
642    /// Create a client view whose routable instances are restricted by a caller-owned
643    /// admission set.
644    ///
645    /// Endpoint discovery remains the source of connection metadata and hard availability. The
646    /// returned client publishes only the intersection of that endpoint membership and
647    /// `admitted_ids`, allowing a higher-level controller to keep discovered-but-unvalidated
648    /// instances out of a routing group. The view has independent overload and fault-inhibition
649    /// state, just like a freshly constructed client.
650    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    /// Like [`Self::with_admitted_instances`], with a lifecycle token for construction-time
661    /// cancellation by an owning controller.
662    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    /// Subscribe to raw discovery events for this endpoint.
731    ///
732    /// Unlike `instance_source`, this feed does not coalesce remove→add pairs,
733    /// so consumers can react to every removal event exactly once.
734    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    /// Wait for at least one Instance to be available for this Endpoint
742    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        // wait for there to be 1 or more endpoints
749        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    /// Wait for at least one discovered instance to be published as routable.
767    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            // Discovery and routing publish on separate tasks. Returning only
776            // after their views overlap prevents a subsequent route seeing no workers.
777            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    /// Mark an instance as down/unavailable
802    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    /// Replace the set of overloaded instances reported by the worker monitor.
816    /// Returns true when this changes the effective overloaded set.
817    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    /// Whether an unexpired request-path overload lease remains after the
823    /// monitor's most recent metric publication.
824    pub fn overload_reconciliation_needed(&self) -> bool {
825        self.routing_instances.overload_reconciliation_needed()
826    }
827
828    /// Mark an instance overloaded immediately after a worker-scoped
829    /// `WorkerOverloaded` response. This is backpressure, not a fault, so it
830    /// does not call `report_instance_down`. A fresh worker-monitor publication
831    /// clears this hint early. Its bounded lease otherwise permits a recovery probe.
832    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    /// Workers currently eligible for selection: discovered and not locally
851    /// inhibited by [`Self::report_instance_down`].
852    ///
853    /// This hard-availability snapshot is separate from transient overload.
854    /// `None` means this client has not discovered an instance yet. After the
855    /// first discovery, `Some` is authoritative, including `Some(empty)` when
856    /// the last previously discovered worker is removed.
857    pub fn available_instance_ids(&self) -> Option<Arc<HashSet<u64>>> {
858        self.routing_instances.available_ids()
859    }
860
861    /// Monitor the key-value instance source and update instance_avail.
862    ///
863    /// This function also performs periodic reconciliation: if `instance_source` hasn't
864    /// changed for `reconcile_interval`, we reset `instance_avail` to match
865    /// `instance_source`. This ensures instances removed via `report_instance_down`
866    /// are eventually restored even if the discovery source doesn't emit updates.
867    ///
868    /// The spawned task runs until `cancel_token` cancels. A caller that wants
869    /// this task to outlive nothing shorter than the process should pass
870    /// `self.endpoint.drt().primary_token()`, as [`Self::new`] does.
871    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                // Clean up stale occupancy counters for instances that no longer exist.
894                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    /// Simulate a complete discovery snapshot for testing.
927    #[cfg(any(test, feature = "testing"))]
928    pub fn override_discovered_instances(&self, ids: Vec<u64>) {
929        self.reconcile_discovered_instances(ids);
930    }
931
932    /// Override routable IDs for testing while preserving discovery membership.
933    #[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        // Discovery::list_and_watch establishes the backend watch, so it runs outside the
953        // registry lock. Holding the lock across it serializes every client construction behind
954        // one round trip to the discovery backend.
955        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        // Another caller can register a source for this endpoint while this watch is
965        // established. Every later client shares that source, and this one drops, which stops
966        // both the watcher task it spawned and the backend watch it established.
967        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    /// Pass a child of the runtime's primary token as `cancel_token`: the backends fall back to
975    /// that token, so shutdown still ends a watch that clients are holding.
976    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    /// A backend whose watch producer, like the Kubernetes watcher, parks on its own feed and ends
1075    /// only when the token it was given cancels. A test sends events to a watch through `feeds`.
1076    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        // Discovery publishes first; the routing monitor has not consumed it yet.
1262        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        // Every client passes the registry lookup before the first one establishes its watch, so
1293        // they all take the slow path and race on the insert.
1294        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    /// Test that instances removed via report_instance_down are restored after
1495    /// the reconciliation interval elapses.
1496    #[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        // Use process_local config to avoid needing etcd/nats
1502        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        // Use a short reconcile interval for faster tests
1510        let client = Client::with_reconcile_interval(endpoint, TEST_RECONCILE_INTERVAL)
1511            .await
1512            .unwrap();
1513
1514        // Initially, instance_avail should be empty (no registered instances)
1515        assert!(client.instance_ids_avail().is_empty());
1516
1517        // For this test, we'll directly manipulate instance_avail and verify reconciliation
1518        // Store some test IDs
1519        client.override_instance_avail(vec![1, 2, 3]);
1520
1521        assert_eq!(client.instance_ids_avail(), vec![1u64, 2, 3]);
1522
1523        // Simulate report_instance_down removing instance 2
1524        client.report_instance_down(2);
1525        assert_eq!(client.instance_ids_avail(), vec![1u64, 3]);
1526
1527        // Wait for reconciliation interval + buffer
1528        // The monitor_instance_source will reset instance_avail to match instance_source
1529        // Since instance_source is empty, after reconciliation instance_avail should be empty
1530        tokio::time::sleep(TEST_RECONCILE_INTERVAL + Duration::from_millis(50)).await;
1531
1532        // After reconciliation, instance_avail should match instance_source (which is empty)
1533        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    /// A zero inhibited duration disables local worker inhibition.
1542    #[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    /// Test that report_instance_down correctly removes an instance from instance_avail.
1571    #[tokio::test]
1572    async fn test_report_instance_down() {
1573        let rt = Runtime::from_current().unwrap();
1574        // Use process_local config to avoid needing etcd/nats
1575        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        // Manually set up instance_avail with test instances
1585        client.override_instance_avail(vec![1, 2, 3]);
1586        assert_eq!(client.instance_ids_avail(), vec![1u64, 2, 3]);
1587
1588        // Report instance 2 as down
1589        client.report_instance_down(2);
1590
1591        // Verify instance 2 is removed
1592        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    /// Test that instance_avail_watcher receives updates when instances change.
1918    #[tokio::test]
1919    async fn test_instance_avail_watcher() {
1920        let rt = Runtime::from_current().unwrap();
1921        // Use process_local config to avoid needing etcd/nats
1922        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        // Set initial instances
1933        client.override_instance_avail(vec![1, 2, 3]);
1934
1935        // Report instance down - this should notify the watcher
1936        client.report_instance_down(2);
1937
1938        // The watcher should receive the update
1939        // Note: We need to check if changed() was signaled
1940        let current = watcher.borrow().clone();
1941        assert_eq!(current, vec![1, 3]);
1942
1943        rt.shutdown();
1944    }
1945
1946    /// Regression test: `monitor_instance_source_with_cancellation`'s task must
1947    /// exit on its own `cancel_token`, not only at process shutdown.
1948    ///
1949    /// `Client::new` bound this task to the process-wide primary token
1950    /// unconditionally. A caller building a `Client` scoped to something
1951    /// narrower — a monitor bound to one `WorkerSet`'s lifecycle, say — had no
1952    /// way to stop the task before then: dropping every `Client` handle does
1953    /// not stop it, since it holds its own clone. Every WorkerSet rebuild
1954    /// leaked one.
1955    ///
1956    /// The observable is the strong count of `routing_instances`: the spawned
1957    /// task captures a clone of it, so the count returning to 1 proves the
1958    /// task actually exited and dropped that capture.
1959    #[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        // Negative control, first: the task must still be alive, and still
1977        // holding its capture, before cancellation.
1978        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    /// Test that concurrent select_and_increment distributes load correctly.
2032    #[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        // state2 should see the same counts (same underlying Arc)
2107        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        // Add some connections
2122        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        // Each instance should have 1 connection
2126        assert_eq!(state.load(1), 1);
2127        assert_eq!(state.load(2), 1);
2128        assert_eq!(state.load(3), 1);
2129
2130        // Discovery removal must not delete guard-owned accounting.
2131        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}