Skip to main content

pingora_load_balancing/
lib.rs

1// Copyright 2026 Cloudflare, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! # Pingora Load Balancing utilities
16//! This crate provides common service discovery, health check and load balancing
17//! algorithms for proxies to use.
18//!
19//! ## Grouped selector internals
20//!
21//! In `LoadBalancerGroup<S>`, `S` is the selector algorithm and its built data,
22//! such as a Ketama ring. The other types manage its configuration, rebuilding,
23//! publication, and lifetime.
24//!
25//! ```text
26//! LoadBalancerGroup<S>
27//! |-- BackendView (Backends)
28//! |   |-- ServiceDiscovery
29//! |   `-- Arc<HealthRegistry>
30//! |-- SelectorRebuildGate
31//! |-- SelectorRebuildCancellation
32//! `-- SelectorSlot<S> x N
33//!     |-- config
34//!     |-- SelectorRebuildState
35//!     |   `-- pending SelectorRebuildRequest
36//!     `-- ArcSwap<PublishedSelector<S>>
37//!         |-- Arc<S>
38//!         |-- readiness snapshot
39//!         `-- SelectorReleaseGuard
40//!             `-- SelectorReleaseSignal
41//!
42//! Shared mode:
43//! HealthCheckService ---> Arc<HealthRegistry> <--- other BackendViews
44//! ```
45//!
46//! The main roles are:
47//!
48//! - `Backends`, also named `BackendView`, owns discovered membership,
49//!   enablement, health references, and the membership generation. Each
50//!   published group selector owns the readiness snapshot for its generation,
51//!   so older selectors keep serving their own snapshot.
52//! - `HealthRegistry` reconciles the targets contributed by its views and owns
53//!   one health state and probe target per backend equivalence key.
54//! - `HealthCheckService` runs one active health-check loop for a shared
55//!   registry. Views with private registries are checked by their load balancer.
56//! - `SelectorSlot<S>` owns one selector configuration, its published selector,
57//!   generation, pending work, timings, and counters.
58//! - `SelectorRebuildRequest` contains the backend membership, its readiness
59//!   snapshot, and generation to build.
60//! - `SelectorRebuildState` tracks the active rebuild task and newest pending
61//!   request.
62//! - `SelectorRebuildTaskGuard` clears the running state and restores an
63//!   in-flight request if the task exits unexpectedly.
64//! - `SelectorRebuildCancellation` stops rebuild tasks when the group is
65//!   dropped.
66//! - `PublishedSelector<S>` pairs the selector exposed to requests with the
67//!   readiness snapshot for its generation and its lifetime tracking.
68//! - `SelectorReleaseGuard` and `SelectorReleaseSignal` notify the gate after a
69//!   replaced selector and all its readers are gone.
70//! - `SelectorRebuildGate` allows one build at a time and prevents another build
71//!   while an old selector is still being destroyed.
72//!
73//! ### Discovery and shared-health flow
74//!
75//! 1. `LoadBalancerGroup<S>` asks its `BackendView` to update.
76//! 2. The view's `ServiceDiscovery` returns its current `Backend` membership and
77//!    enablement.
78//! 3. `BackendView` publishes that membership and updates its contribution to
79//!    `HealthRegistry`.
80//! 4. `HealthRegistry` reconciles the targets from all of its views.
81//! 5. In shared mode, `HealthCheckService` probes each registry target once.
82//! 6. The resulting health state is visible through every contributing view.
83//! 7. Each view applies its own membership and enablement. Each rebuilt group
84//!    selector is published with the readiness snapshot for its generation.
85//!
86//! ### Selector rebuild flow
87//!
88//! 1. `Backends` advances the membership generation and produces an indivisible
89//!    membership and readiness update bundle.
90//! 2. `LoadBalancerGroup<S>` schedules each selector rebuild from that bundle.
91//! 3. `SelectorRebuildState` keeps one active rebuild and coalesces newer work
92//!    into its pending request.
93//! 4. `SelectorRebuildTaskGuard` tracks the in-flight request while the task
94//!    acquires `SelectorRebuildGate`.
95//! 5. The task builds the selector `S` from the request's backend snapshot.
96//! 6. A new `PublishedSelector<S>` is stored in the slot's `ArcSwap`, replacing
97//!    the old published selector atomically.
98//! 7. The slot publishes its selector generation and can process its next
99//!    pending request.
100//!
101//! ### Request flow
102//!
103//! 1. `LoadBalancerGroup<S>::select` loads a `PublishedSelector<S>` from the
104//!    chosen `SelectorSlot<S>`.
105//! 2. That published selector snapshot is held for the whole selection while it
106//!    yields ordered backend candidates.
107//! 3. The published selector's own readiness snapshot answers enablement and
108//!    health for each candidate.
109//! 4. The first accepted backend is returned; otherwise selection returns
110//!    `None`.
111//! 5. Replacing the selector does not affect this request's iterator.
112//! 6. When the final reader releases the old `PublishedSelector<S>`, its
113//!    `SelectorReleaseGuard` updates `SelectorReleaseSignal`.
114//! 7. `SelectorRebuildGate` observes that signal and permits the next build.
115//!
116//! ### Cancellation flow
117//!
118//! 1. Dropping `LoadBalancerGroup<S>` triggers `SelectorRebuildCancellation`.
119//! 2. A task waiting for `SelectorRebuildGate` exits without removing the old
120//!    selector's `SelectorReleaseSignal`.
121//! 3. `SelectorRebuildTaskGuard` restores an in-flight
122//!    `SelectorRebuildRequest` unless a newer request already replaced it.
123//! 4. A built but unpublished selector is destroyed on a blocking worker.
124//! 5. That destruction retains the gate permit, so another group cannot build
125//!    at the same time.
126//! 6. The task guard clears the slot's running state and notifies waiters.
127//! 7. After destruction completes, the gate permit is released.
128
129// https://github.com/mcarton/rust-derivative/issues/112
130// False positive for macro generated code
131#![allow(clippy::non_canonical_partial_ord_impl)]
132
133use arc_swap::ArcSwap;
134use derivative::Derivative;
135use futures::FutureExt;
136pub use http::Extensions;
137use pingora_core::protocols::l4::socket::SocketAddr;
138use pingora_error::{ErrorType, OrErr, Result};
139use std::collections::hash_map::DefaultHasher;
140use std::collections::{BTreeMap, BTreeSet, HashMap};
141use std::future::Future;
142use std::hash::{Hash, Hasher};
143use std::io::Result as IoResult;
144use std::net::ToSocketAddrs;
145use std::sync::atomic::{
146    AtomicBool, AtomicU64,
147    Ordering::{Acquire, Relaxed, Release},
148};
149use std::sync::{mpsc, Arc, Mutex, MutexGuard, OnceLock, Weak};
150use std::time::{Duration, Instant};
151use tokio::sync::{Notify, OwnedSemaphorePermit, Semaphore};
152
153mod background;
154pub mod discovery;
155pub mod health_check;
156pub mod selection;
157
158use discovery::ServiceDiscovery;
159use health_check::Health;
160use selection::UniqueIterator;
161use selection::{BackendIter, BackendSelection};
162
163pub mod prelude {
164    pub use crate::health_check::TcpHealthCheck;
165    pub use crate::selection::RoundRobin;
166    pub use crate::{BackendView, HealthCheckService, HealthRegistry, LoadBalancer};
167}
168
169/// [Backend] represents a server to proxy or connect to.
170#[derive(Derivative)]
171#[derivative(Clone, Hash, PartialEq, PartialOrd, Eq, Ord, Debug)]
172pub struct Backend {
173    /// The address to the backend server.
174    pub addr: SocketAddr,
175    /// The relative weight of the server. Load balancing algorithms will
176    /// proportionally distributed traffic according to this value.
177    pub weight: usize,
178
179    /// The extension field to put arbitrary data to annotate the Backend.
180    /// The data added here is opaque to this crate hence the data is ignored by
181    /// functionalities of this crate. For example, two backends with the same
182    /// [SocketAddr] and the same weight but different `ext` data are considered
183    /// identical.
184    /// See [Extensions] for how to add and read the data.
185    #[derivative(PartialEq = "ignore")]
186    #[derivative(PartialOrd = "ignore")]
187    #[derivative(Hash = "ignore")]
188    #[derivative(Ord = "ignore")]
189    pub ext: Extensions,
190}
191
192impl Backend {
193    /// Create a new [Backend] with `weight` 1. The function will try to parse
194    ///  `addr` into a [std::net::SocketAddr].
195    pub fn new(addr: &str) -> Result<Self> {
196        Self::new_with_weight(addr, 1)
197    }
198
199    /// Creates a new [Backend] with the specified `weight`. The function will try to parse
200    /// `addr` into a [std::net::SocketAddr].
201    pub fn new_with_weight(addr: &str, weight: usize) -> Result<Self> {
202        let addr = addr
203            .parse()
204            .or_err(ErrorType::InternalError, "invalid socket addr")?;
205        Ok(Backend {
206            addr: SocketAddr::Inet(addr),
207            weight,
208            ext: Extensions::new(),
209        })
210        // TODO: UDS
211    }
212
213    pub(crate) fn hash_key(&self) -> u64 {
214        let mut hasher = DefaultHasher::new();
215        self.hash(&mut hasher);
216        hasher.finish()
217    }
218}
219
220impl std::ops::Deref for Backend {
221    type Target = SocketAddr;
222
223    fn deref(&self) -> &Self::Target {
224        &self.addr
225    }
226}
227
228impl std::ops::DerefMut for Backend {
229    fn deref_mut(&mut self) -> &mut Self::Target {
230        &mut self.addr
231    }
232}
233
234impl std::net::ToSocketAddrs for Backend {
235    type Iter = std::iter::Once<std::net::SocketAddr>;
236
237    fn to_socket_addrs(&self) -> std::io::Result<Self::Iter> {
238        self.addr.to_socket_addrs()
239    }
240}
241
242/// The backends to check and their health.
243///
244/// Both are updated together. If multiple backends have the same health key,
245/// only one is checked and they all share one health value.
246struct HealthRegistryState {
247    targets: Box<[Backend]>,
248    /// Health handles keyed by the registry's health key (see
249    /// [`HealthRegistry::health_key`]). The default key uses full backend
250    /// identity; callers can supply a different key when constructing a
251    /// registry.
252    health: HashMap<u64, Health>,
253}
254
255/// Active health state shared by one or more [`BackendView`]s.
256///
257/// Registries use full backend identity by default. Callers can supply an
258/// equivalence key to intentionally share probes across backend variants.
259pub struct HealthRegistry {
260    health_check: OnceLock<Arc<dyn health_check::HealthCheck + Send + Sync + 'static>>,
261    state: ArcSwap<HealthRegistryState>,
262    /// View changes are applied one at a time so concurrent updates are not
263    /// lost. Request handling does not read this map.
264    views: Mutex<BTreeMap<u64, Arc<BTreeSet<Backend>>>>,
265    /// Assigns each view the ID used to update and remove its backend set.
266    next_view_id: AtomicU64,
267    /// Queues view removals so dropping a view never waits for reconciliation.
268    view_removal_tx: mpsc::Sender<u64>,
269    /// View removals waiting to be applied during the next registry operation.
270    pending_view_removals: Mutex<mpsc::Receiver<u64>>,
271    /// Wakes the health service so queued removals are applied promptly.
272    view_removed: Notify,
273    /// Wakes the health service when the registry gains its first target.
274    targets_available: Notify,
275    /// Backends with the same key share one health check and health value.
276    equivalence_key: Arc<dyn Fn(&Backend) -> u64 + Send + Sync + 'static>,
277}
278
279impl Default for HealthRegistry {
280    fn default() -> Self {
281        Self::new()
282    }
283}
284
285impl HealthRegistry {
286    /// Create an empty registry keyed by full backend identity.
287    pub fn new() -> Self {
288        Self::with_equivalence(|backend| backend.hash_key())
289    }
290
291    /// Create an empty registry using `equivalence_key` to group targets.
292    ///
293    /// Backends that produce the same `u64` key share one health state and
294    /// probe. Callers are responsible for ensuring that backends which need
295    /// independent health state produce distinct keys.
296    pub fn with_equivalence(
297        equivalence_key: impl Fn(&Backend) -> u64 + Send + Sync + 'static,
298    ) -> Self {
299        let (view_removal_tx, pending_view_removals) = mpsc::channel();
300        Self {
301            health_check: OnceLock::new(),
302            state: ArcSwap::new(Arc::new(HealthRegistryState {
303                targets: Vec::new().into_boxed_slice(),
304                health: HashMap::new(),
305            })),
306            views: Mutex::new(BTreeMap::new()),
307            next_view_id: AtomicU64::new(1),
308            view_removal_tx,
309            pending_view_removals: Mutex::new(pending_view_removals),
310            view_removed: Notify::new(),
311            targets_available: Notify::new(),
312            equivalence_key: Arc::new(equivalence_key),
313        }
314    }
315
316    /// The key under which a backend's shared [`Health`] is tracked.
317    fn health_key(&self, backend: &Backend) -> u64 {
318        (self.equivalence_key)(backend)
319    }
320
321    /// Set the health-check implementation used by every view in this registry.
322    ///
323    /// # Panics
324    ///
325    /// Panics if a health check has already been set.
326    pub fn set_health_check(&self, hc: Box<dyn health_check::HealthCheck + Send + Sync + 'static>) {
327        assert!(
328            self.health_check.set(hc.into()).is_ok(),
329            "health check already configured"
330        );
331    }
332
333    fn has_health_check(&self) -> bool {
334        self.health_check.get().is_some()
335    }
336
337    fn register_view(&self) -> u64 {
338        self.apply_pending_view_removals();
339        let view_id = self.next_view_id.fetch_add(1, Relaxed);
340        self.views
341            .lock()
342            .unwrap_or_else(|poisoned| poisoned.into_inner())
343            .insert(view_id, Arc::new(BTreeSet::new()));
344        view_id
345    }
346
347    /// Publish `backends` as `view_id`'s membership and return the reconciled
348    /// registry state so the caller can snapshot the shared health handles for
349    /// its backends without a second registry load.
350    fn update_view(
351        &self,
352        view_id: u64,
353        backends: Arc<BTreeSet<Backend>>,
354    ) -> Arc<HealthRegistryState> {
355        self.apply_pending_view_removals();
356        let mut views = self
357            .views
358            .lock()
359            .unwrap_or_else(|poisoned| poisoned.into_inner());
360        views.insert(view_id, backends);
361        self.reconcile(&views)
362    }
363
364    fn defer_view_removal(&self, view_id: u64) {
365        let _ = self.view_removal_tx.send(view_id);
366        self.view_removed.notify_one();
367    }
368
369    fn apply_pending_view_removals(&self) {
370        let view_ids: Vec<_> = self
371            .pending_view_removals
372            .lock()
373            .unwrap_or_else(|poisoned| poisoned.into_inner())
374            .try_iter()
375            .collect();
376        if view_ids.is_empty() {
377            return;
378        }
379
380        let mut views = self
381            .views
382            .lock()
383            .unwrap_or_else(|poisoned| poisoned.into_inner());
384        let mut changed = false;
385        for view_id in view_ids {
386            changed |= views.remove(&view_id).is_some();
387        }
388        if changed {
389            self.reconcile(&views);
390        }
391    }
392
393    /// Wait until a view removal is queued. The caller must then apply pending removals.
394    pub(crate) async fn wait_for_view_removal(&self) {
395        self.view_removed.notified().await;
396    }
397
398    fn reconcile(&self, views: &BTreeMap<u64, Arc<BTreeSet<Backend>>>) -> Arc<HealthRegistryState> {
399        let old_state = self.state.load();
400        let mut targets_by_key = HashMap::new();
401        for backends in views.values() {
402            for backend in backends.iter() {
403                targets_by_key
404                    .entry(self.health_key(backend))
405                    .or_insert_with(|| backend.clone());
406            }
407        }
408
409        let mut targets: Vec<_> = targets_by_key.into_values().collect();
410        targets.sort_unstable();
411        let gained_first_target = old_state.targets.is_empty() && !targets.is_empty();
412        let mut health = HashMap::with_capacity(targets.len());
413        for backend in &targets {
414            let key = self.health_key(backend);
415            health.insert(key, old_state.health.get(&key).cloned().unwrap_or_default());
416        }
417
418        let new_state = Arc::new(HealthRegistryState {
419            targets: targets.into_boxed_slice(),
420            health,
421        });
422        self.state.store(Arc::clone(&new_state));
423        if gained_first_target {
424            self.targets_available.notify_one();
425        }
426        new_state
427    }
428
429    /// Return the number of distinct backend targets currently being tracked.
430    ///
431    /// Targets that produce the same registry health key count as one target.
432    pub fn target_count(&self) -> usize {
433        self.apply_pending_view_removals();
434        self.state.load().targets.len()
435    }
436
437    /// Wait until at least one registered view contributes a backend target.
438    pub(crate) async fn wait_for_targets(&self) {
439        loop {
440            let notified = self.targets_available.notified();
441            // `enable()` requires a pinned future because it registers it in
442            // `Notify`'s waiter list. Register first so a new target is not missed.
443            tokio::pin!(notified);
444            notified.as_mut().enable();
445            if self.target_count() > 0 {
446                return;
447            }
448            notified.await;
449        }
450    }
451
452    /// Run one active health-check pass over the union of all registered views.
453    ///
454    /// When `parallel` is true, all targets are checked concurrently.
455    ///
456    /// Dropping the returned future cancels the pass: outstanding per-target
457    /// checks are aborted rather than left running to completion, so targets
458    /// not yet reported keep their previous health.
459    pub async fn run_health_check(&self, parallel: bool) {
460        use crate::health_check::HealthCheck;
461        use log::{info, warn};
462        use pingora_runtime::current_handle;
463
464        async fn check_and_report(
465            backend: &Backend,
466            check: &Arc<dyn HealthCheck + Send + Sync>,
467            health: &Health,
468        ) {
469            let errored = check.check(backend).await.err();
470            let healthy = errored.is_none();
471            let flipped = health.observe_health(healthy, check.health_threshold(healthy));
472            if flipped {
473                check.health_status_change(backend, healthy).await;
474                let summary = check.backend_summary(backend);
475                if let Some(error) = errored {
476                    warn!("{summary} becomes unhealthy, {error}");
477                } else {
478                    info!("{summary} becomes healthy");
479                }
480            }
481        }
482
483        self.apply_pending_view_removals();
484        let Some(health_check) = self.health_check.get().cloned() else {
485            // nothing to do
486            return;
487        };
488
489        let state = self.state.load_full();
490        if parallel {
491            let runtime = current_handle();
492            // A `JoinSet` aborts every task it still owns when it is dropped, so
493            // abandoning this pass mid-flight cancels the outstanding checks
494            // instead of leaving them detached on the runtime.
495            let mut jobs = tokio::task::JoinSet::new();
496            for backend in state.targets.iter() {
497                let backend = backend.clone();
498                let key = self.health_key(&backend);
499                let check = Arc::clone(&health_check);
500                let state = Arc::clone(&state);
501                jobs.spawn_on(
502                    async move {
503                        if let Some(health) = state.health.get(&key) {
504                            check_and_report(&backend, &check, health).await;
505                        }
506                    },
507                    &runtime,
508                );
509            }
510            // Drained one at a time rather than with `JoinSet::join_all`, which
511            // resumes a task panic in this caller. One backend's check panicking
512            // should not abort the checks for every other backend.
513            while let Some(joined) = jobs.join_next().await {
514                if let Err(error) = joined {
515                    // That backend keeps its previous health for this pass.
516                    warn!("health check task failed: {error}");
517                }
518            }
519        } else {
520            for backend in state.targets.iter() {
521                if let Some(health) = state.health.get(&self.health_key(backend)) {
522                    check_and_report(backend, &health_check, health).await;
523                }
524            }
525        }
526    }
527}
528
529/// View-local enablement paired with the shared [`Health`] handle for one
530/// backend identity.
531///
532/// Caching the [`Health`] handle in a [`ReadinessSnapshot`] lets readiness be
533/// answered with a single map lookup, avoiding a snapshot of the
534/// [`HealthRegistry`]. The handle shares its inner state with the registry, so
535/// health observations and reconciliation remain visible through it, including
536/// through a snapshot retained by an older selector.
537#[derive(Clone)]
538struct BackendReadiness {
539    enabled: Arc<AtomicBool>,
540    health: Health,
541}
542
543impl BackendReadiness {
544    fn ready(&self) -> bool {
545        self.enabled.load(Relaxed) && self.health.ready()
546    }
547}
548
549/// An immutable, readiness-only snapshot of one backend view generation.
550///
551/// Readiness is indexed by [`Backend::hash_key`]. Cloning shares the underlying
552/// map through an [`Arc`]; each [`BackendReadiness`] keeps the shared live
553/// [`Health`] handle and the [`struct@AtomicBool`] enablement flag, so health
554/// observations and manual enable/disable stay visible through every clone of a
555/// generation's snapshot, including one owned by an older selector.
556#[derive(Clone)]
557struct ReadinessSnapshot(Arc<HashMap<u64, BackendReadiness>>);
558
559impl ReadinessSnapshot {
560    /// A snapshot with no known backends.
561    fn empty() -> Self {
562        Self(Arc::new(HashMap::new()))
563    }
564
565    /// Look up readiness by full backend identity.
566    fn get(&self, backend: &Backend) -> Option<&BackendReadiness> {
567        self.0.get(&backend.hash_key())
568    }
569
570    /// Whether the backend is present in this snapshot and both enabled and
571    /// healthy.
572    fn ready(&self, backend: &Backend) -> bool {
573        self.get(backend).is_some_and(BackendReadiness::ready)
574    }
575}
576
577/// One published snapshot of a backend view: its current membership paired with
578/// the readiness snapshot for that generation.
579struct BackendViewState {
580    /// The backend membership exposed by this snapshot.
581    backends: Arc<BTreeSet<Backend>>,
582    /// Readiness for the current membership, keyed by full backend identity.
583    readiness: ReadinessSnapshot,
584}
585
586/// An indivisible membership and readiness update produced by a membership
587/// change.
588///
589/// Groups schedule selector rebuilds from this bundle so each rebuilt selector
590/// is published with the exact readiness snapshot for its generation, keeping
591/// membership, readiness, and generation consistent across coalescing, retries,
592/// and cancellation.
593struct BackendUpdate {
594    /// Membership generation this update advanced to.
595    generation: u64,
596    /// Membership captured for this generation.
597    backends: Arc<BTreeSet<Backend>>,
598    /// Readiness snapshot for this generation.
599    readiness: ReadinessSnapshot,
600}
601
602/// A discovered backend membership with view-local enablement and shared
603/// active health state.
604///
605/// Readiness is the conjunction of current view membership, view-local
606/// enablement, and the health state in the associated [`HealthRegistry`].
607pub struct Backends {
608    discovery: Box<dyn ServiceDiscovery + Send + Sync + 'static>,
609    health_registry: Arc<HealthRegistry>,
610    view_id: u64,
611    state: ArcSwap<BackendViewState>,
612    /// Membership generation, advanced on each change.
613    generation: AtomicU64,
614    /// Weak enablement handles keyed by full backend identity.
615    ///
616    /// Lets [`Backends::set_enable`] reach a removed backend whose enablement
617    /// flag is still held by an older selector's readiness snapshot, and lets a
618    /// re-added backend recover its manual enablement while an older selector
619    /// still references it. Never read on the request path, so it takes a lock
620    /// only during membership updates and manual enable/disable. Expired entries
621    /// are cleaned up opportunistically on each membership change.
622    enablement_handles: Mutex<HashMap<u64, Weak<AtomicBool>>>,
623    /// Whether the load balancer owning this view owns its active health checks.
624    /// Shared-registry views leave them to [`HealthCheckService`].
625    owns_health_checks: bool,
626}
627
628/// Explicit name for a [`Backends`] instance used as one membership view.
629pub type BackendView = Backends;
630
631impl Backends {
632    /// Create a backend view with a private health registry.
633    ///
634    /// Load balancers constructed from this view retain the existing behavior
635    /// of scheduling their own health checks.
636    pub fn new(discovery: Box<dyn ServiceDiscovery + Send + Sync + 'static>) -> Self {
637        Self::new_inner(discovery, Arc::new(HealthRegistry::new()), true)
638    }
639
640    /// Create a backend view that contributes targets to `health_registry`.
641    ///
642    /// Health checks for shared views must be scheduled once through
643    /// [`HealthCheckService`] instead of by every load balancer using the view.
644    /// The registry must have a health check configured before that service is
645    /// started.
646    pub fn new_with_health_registry(
647        discovery: Box<dyn ServiceDiscovery + Send + Sync + 'static>,
648        health_registry: Arc<HealthRegistry>,
649    ) -> Self {
650        Self::new_inner(discovery, health_registry, false)
651    }
652
653    fn new_inner(
654        discovery: Box<dyn ServiceDiscovery + Send + Sync + 'static>,
655        health_registry: Arc<HealthRegistry>,
656        owns_health_checks: bool,
657    ) -> Self {
658        let view_id = health_registry.register_view();
659        Self {
660            discovery,
661            health_registry,
662            view_id,
663            state: ArcSwap::new(Arc::new(BackendViewState {
664                backends: Arc::new(BTreeSet::new()),
665                readiness: ReadinessSnapshot::empty(),
666            })),
667            generation: AtomicU64::new(0),
668            enablement_handles: Mutex::new(HashMap::new()),
669            owns_health_checks,
670        }
671    }
672
673    /// The current membership generation, advanced once per membership change.
674    pub(crate) fn generation(&self) -> u64 {
675        self.generation.load(Relaxed)
676    }
677
678    fn lock_enablement_handles(&self) -> MutexGuard<'_, HashMap<u64, Weak<AtomicBool>>> {
679        self.enablement_handles
680            .lock()
681            .unwrap_or_else(|poisoned| poisoned.into_inner())
682    }
683
684    /// The readiness snapshot for the currently published generation.
685    fn readiness_snapshot(&self) -> ReadinessSnapshot {
686        self.state.load().readiness.clone()
687    }
688
689    /// Set the health check used by this view's entire health registry.
690    pub fn set_health_check(
691        &mut self,
692        hc: Box<dyn health_check::HealthCheck + Send + Sync + 'static>,
693    ) {
694        self.health_registry.set_health_check(hc);
695    }
696
697    fn do_update<F>(
698        &self,
699        new_backends: BTreeSet<Backend>,
700        enablement: HashMap<u64, bool>,
701        callback: F,
702    ) -> Option<BackendUpdate>
703    where
704        F: FnOnce(Arc<BTreeSet<Backend>>),
705    {
706        let old_state = self.state.load_full();
707        let membership_changed = *old_state.backends != new_backends;
708        if membership_changed {
709            let generation = self.generation.fetch_add(1, Relaxed) + 1;
710            let new_backends = Arc::new(new_backends);
711            let registry_state = self
712                .health_registry
713                .update_view(self.view_id, Arc::clone(&new_backends));
714
715            let mut new_readiness = HashMap::with_capacity(new_backends.len());
716            {
717                let mut handles = self.lock_enablement_handles();
718                // Opportunistically drop enablement handles that no readiness
719                // snapshot references anymore.
720                handles.retain(|_, weak| weak.strong_count() > 0);
721                for backend in new_backends.iter() {
722                    let key = backend.hash_key();
723                    // Preserve enablement across removal and re-addition while
724                    // an older selector's snapshot still holds the flag.
725                    let enabled = old_state
726                        .readiness
727                        .get(backend)
728                        .map(|current| Arc::clone(&current.enabled))
729                        .or_else(|| handles.get(&key).and_then(Weak::upgrade))
730                        .unwrap_or_else(|| Arc::new(AtomicBool::new(true)));
731                    if let Some(enabled_override) = enablement.get(&key) {
732                        enabled.store(*enabled_override, Relaxed);
733                    }
734                    handles.insert(key, Arc::downgrade(&enabled));
735                    let health = registry_state
736                        .health
737                        .get(&self.health_registry.health_key(backend))
738                        .cloned()
739                        .unwrap_or_default();
740                    new_readiness.insert(key, BackendReadiness { enabled, health });
741                }
742            }
743            let new_readiness = ReadinessSnapshot(Arc::new(new_readiness));
744
745            // Cover both the old and new readiness during a synchronous selector
746            // rebuild so a request in the callback does not lose a backend the
747            // about-to-be-replaced selector may still yield. This allocation is
748            // linear in old and new membership and only runs on membership changes.
749            let mut transition = old_state.readiness.0.as_ref().clone();
750            for (key, readiness) in new_readiness.0.iter() {
751                transition.insert(*key, readiness.clone());
752            }
753            self.state.store(Arc::new(BackendViewState {
754                backends: Arc::clone(&old_state.backends),
755                readiness: ReadinessSnapshot(Arc::new(transition)),
756            }));
757
758            callback(Arc::clone(&new_backends));
759
760            self.state.store(Arc::new(BackendViewState {
761                backends: Arc::clone(&new_backends),
762                readiness: new_readiness.clone(),
763            }));
764            Some(BackendUpdate {
765                generation,
766                backends: new_backends,
767                readiness: new_readiness,
768            })
769        } else {
770            for (key, enabled) in enablement {
771                if let Some(current) = old_state.readiness.0.get(&key) {
772                    current.enabled.store(enabled, Relaxed);
773                }
774            }
775            None
776        }
777    }
778
779    /// Whether `backend` is enabled and healthy in this view's current
780    /// membership.
781    ///
782    /// This is on the hot request path: it takes a single snapshot of the
783    /// published view state and performs one map lookup to a `BackendReadiness`
784    /// carrying both the view-local enablement flag and the shared `Health`
785    /// handle, so no second registry snapshot is required.
786    ///
787    /// Readiness is keyed by full backend identity. Only the current membership
788    /// is reported: a removed backend is not ready here even while an older
789    /// group selector can still return it through its own readiness snapshot.
790    pub fn ready(&self, backend: &Backend) -> bool {
791        self.state.load().readiness.ready(backend)
792    }
793
794    /// Manually enable or disable `backend` by full backend identity.
795    ///
796    /// The current membership is updated in place. A removed backend is reached
797    /// through the weak enablement-handle interner, so disabling or re-enabling
798    /// a backend still held by an older group selector's readiness snapshot
799    /// takes effect for that selector too. Not on the request path, so it may
800    /// take the interner lock. If the backend is unknown or no snapshot retains
801    /// its enablement flag, this method does nothing.
802    pub fn set_enable(&self, backend: &Backend, enabled: bool) {
803        let key = backend.hash_key();
804        // Current membership shares the same `Arc<AtomicBool>` as the interner
805        // and any older snapshot, so flipping it here is sufficient.
806        if let Some(readiness) = self.state.load().readiness.0.get(&key) {
807            readiness.enabled.store(enabled, Relaxed);
808            return;
809        }
810        // Otherwise reach a removed backend whose flag an older selector's
811        // snapshot may still hold.
812        if let Some(enabled_flag) = self
813            .lock_enablement_handles()
814            .get(&key)
815            .and_then(Weak::upgrade)
816        {
817            enabled_flag.store(enabled, Relaxed);
818        }
819    }
820
821    /// Return this view's current backend membership.
822    pub fn get_backend(&self) -> Arc<BTreeSet<Backend>> {
823        Arc::clone(&self.state.load().backends)
824    }
825
826    /// Run discovery and invoke `callback` when membership changes.
827    ///
828    /// Calls on the same backend view must not overlap with another update.
829    pub async fn update<F>(&self, callback: F) -> Result<()>
830    where
831        F: FnOnce(Arc<BTreeSet<Backend>>),
832    {
833        let (new_backends, enablement) = self.discovery.discover().await?;
834        self.do_update(new_backends, enablement, callback);
835        Ok(())
836    }
837
838    /// Run discovery and return the membership and readiness update bundle when
839    /// membership changes, for a group to schedule selector rebuilds.
840    async fn update_backends(&self) -> Result<Option<BackendUpdate>> {
841        let (new_backends, enablement) = self.discovery.discover().await?;
842        Ok(self.do_update(new_backends, enablement, |_| {}))
843    }
844
845    /// Run one health-check pass for this view's entire registry.
846    ///
847    /// Dropping the returned future cancels the pass, as described on
848    /// [`HealthRegistry::run_health_check`].
849    pub async fn run_health_check(&self, parallel: bool) {
850        self.health_registry.run_health_check(parallel).await;
851    }
852
853    /// Return whether this view's load balancer owns its active probe loop.
854    fn owns_health_checks(&self) -> bool {
855        self.owns_health_checks
856    }
857}
858
859impl Drop for Backends {
860    fn drop(&mut self) {
861        self.health_registry.defer_view_removal(self.view_id);
862    }
863}
864
865/// Background service that runs one health-check loop for a shared
866/// [`HealthRegistry`].
867///
868/// Use this service with views created by
869/// [`BackendView::new_with_health_registry`]. Load balancers backed by
870/// [`Backends::new`] already schedule their private registry and must not also
871/// run a `HealthCheckService` for it.
872///
873/// In periodic mode (`health_check_frequency` set) the service signals
874/// readiness after its first health-check pass, including when no views have
875/// published targets yet, and publishing the first target wakes it for an
876/// immediate pass. In one-shot mode (`health_check_frequency` is `None`) the
877/// service instead waits for the first published target before running its
878/// single pass and signaling readiness, so it never checks an empty registry
879/// and returns before any target exists.
880pub struct HealthCheckService {
881    registry: Arc<HealthRegistry>,
882    /// How frequently to run health checks.
883    ///
884    /// If `None`, health checks run once when the service starts.
885    pub health_check_frequency: Option<Duration>,
886    /// Whether to check all targets concurrently.
887    pub parallel_health_check: bool,
888}
889
890impl HealthCheckService {
891    /// Create a health-check service for `registry`.
892    ///
893    /// A health check must be configured on `registry` before the service is
894    /// started. Starting without one fails closed without signaling readiness.
895    pub fn new(registry: Arc<HealthRegistry>) -> Self {
896        Self {
897            registry,
898            health_check_frequency: None,
899            parallel_health_check: false,
900        }
901    }
902}
903
904/// Timing information from the most recent [`LoadBalancer::update`] call.
905#[derive(Debug, Clone, Copy)]
906pub struct UpdateTimings {
907    /// Time spent in [`ServiceDiscovery::discover`].
908    pub discovery_duration: Duration,
909    /// Time spent building the selection algorithm and storing the updated backends.
910    ///
911    /// This is zero for [`LoadBalancerGroup`] because its selectors rebuild
912    /// asynchronously. Use
913    /// [`LoadBalancerGroup::selector_last_update_timing`] for per-selector
914    /// queue and build durations.
915    pub build_duration: Duration,
916}
917
918/// A [LoadBalancer] instance contains the service discovery, health check and backend selection
919/// all together.
920///
921/// In order to run service discovery and health check at the designated frequencies, the [LoadBalancer]
922/// needs to be run as a [pingora_core::services::background::BackgroundService].
923pub struct LoadBalancer<S>
924where
925    S: BackendSelection,
926{
927    backends: Backends,
928    selector: ArcSwap<S>,
929
930    config: Option<S::Config>,
931
932    /// Timing information from the most recent [`update`](Self::update) call.
933    ///
934    /// `None` until the first successful update completes.
935    last_update_timing: ArcSwap<Option<UpdateTimings>>,
936
937    /// How frequent the health check logic (if set) should run.
938    ///
939    /// If `None`, the health check logic will only run once at the beginning.
940    /// This setting is ignored for views created with
941    /// [`BackendView::new_with_health_registry`]; use [`HealthCheckService`]
942    /// for those views.
943    pub health_check_frequency: Option<Duration>,
944    /// How frequent the service discovery should run.
945    ///
946    /// If `None`, the service discovery will only run once at the beginning.
947    pub update_frequency: Option<Duration>,
948    /// Whether to run health check to all backends in parallel. Default is false.
949    pub parallel_health_check: bool,
950}
951
952fn build_selector<S>(backends: &BTreeSet<Backend>, config: Option<&S::Config>) -> S
953where
954    S: BackendSelection,
955{
956    if let Some(config) = config {
957        S::build_with_config(backends, config)
958    } else {
959        S::build(backends)
960    }
961}
962
963impl<S> LoadBalancer<S>
964where
965    S: BackendSelection + 'static,
966    S::Iter: BackendIter,
967{
968    /// Build a [LoadBalancer] with static backends created from the iter.
969    ///
970    /// Note: [ToSocketAddrs] will invoke blocking network IO for DNS lookup if
971    /// the input cannot be directly parsed as [SocketAddr].
972    pub fn try_from_iter<A, T: IntoIterator<Item = A>>(iter: T) -> IoResult<Self>
973    where
974        A: ToSocketAddrs,
975    {
976        let discovery = discovery::Static::try_from_iter(iter)?;
977        let backends = Backends::new(discovery);
978        let lb = Self::from_backends(backends);
979        lb.update()
980            .now_or_never()
981            .expect("static should not block")
982            .expect("static should not error");
983        Ok(lb)
984    }
985
986    /// Build a [LoadBalancer] with the given [Backends] and the config.
987    pub fn from_backends_with_config(backends: Backends, config_opt: Option<S::Config>) -> Self {
988        let selector_raw = build_selector::<S>(&backends.get_backend(), config_opt.as_ref());
989
990        let selector = ArcSwap::new(Arc::new(selector_raw));
991
992        LoadBalancer {
993            backends,
994            selector,
995            config: config_opt,
996            last_update_timing: ArcSwap::new(Arc::new(None)),
997            health_check_frequency: None,
998            update_frequency: None,
999            parallel_health_check: false,
1000        }
1001    }
1002
1003    /// Build a [LoadBalancer] with the given [Backends].
1004    pub fn from_backends(backends: Backends) -> Self {
1005        Self::from_backends_with_config(backends, None)
1006    }
1007
1008    /// Run the service discovery and update the selection algorithm.
1009    ///
1010    /// This function will be called every `update_frequency` if this [LoadBalancer] instance
1011    /// is running as a background service.
1012    ///
1013    /// On success, the timing information from this call is stored and can be
1014    /// retrieved via [`last_update_timing`](Self::last_update_timing).
1015    pub async fn update(&self) -> Result<()> {
1016        use std::sync::atomic::{AtomicU64, Ordering::Relaxed};
1017
1018        let build_nanos = AtomicU64::new(0);
1019        let total_start = Instant::now();
1020
1021        self.backends
1022            .update(|backends| {
1023                let build_start = Instant::now();
1024                let selector = build_selector::<S>(&backends, self.config.as_ref());
1025                self.selector.store(Arc::new(selector));
1026                build_nanos.store(build_start.elapsed().as_nanos() as u64, Relaxed);
1027            })
1028            .await?;
1029
1030        let total = total_start.elapsed();
1031        let build = Duration::from_nanos(build_nanos.load(Relaxed));
1032
1033        self.last_update_timing.store(Arc::new(Some(UpdateTimings {
1034            discovery_duration: total.saturating_sub(build),
1035            build_duration: build,
1036        })));
1037
1038        Ok(())
1039    }
1040
1041    /// Return the first healthy [Backend] according to the selection algorithm and the
1042    /// health check results.
1043    ///
1044    /// The `key` is used for hash based selection and is ignored if the selection is random or
1045    /// round robin.
1046    ///
1047    /// the `max_iterations` is there to bound the search time for the next Backend. In certain
1048    /// algorithm like Ketama hashing, the search for the next backend is linear and could take
1049    /// a lot steps.
1050    // TODO: consider remove `max_iterations` as users have no idea how to set it.
1051    pub fn select(&self, key: &[u8], max_iterations: usize) -> Option<Backend> {
1052        self.select_with(key, max_iterations, |_, health| health)
1053    }
1054
1055    /// Similar to [Self::select], return the first healthy [Backend] according to the selection algorithm
1056    /// and the user defined `accept` function.
1057    ///
1058    /// The `accept` function takes two inputs, the backend being selected and the internal health of that
1059    /// backend. The function can do things like ignoring the internal health checks or skipping this backend
1060    /// because it failed before. The `accept` function is called multiple times iterating over backends
1061    /// until it returns `true`.
1062    pub fn select_with<F>(&self, key: &[u8], max_iterations: usize, accept: F) -> Option<Backend>
1063    where
1064        F: Fn(&Backend, bool) -> bool,
1065    {
1066        let selection = self.selector.load();
1067        let mut iter = UniqueIterator::new(selection.iter(key), max_iterations);
1068        while let Some(b) = iter.get_next() {
1069            if accept(&b, self.backends.ready(&b)) {
1070                return Some(b);
1071            }
1072        }
1073        None
1074    }
1075
1076    /// Set the health check method. See [health_check].
1077    pub fn set_health_check(
1078        &mut self,
1079        hc: Box<dyn health_check::HealthCheck + Send + Sync + 'static>,
1080    ) {
1081        self.backends.set_health_check(hc);
1082    }
1083
1084    /// Access the [Backends] of this [LoadBalancer]
1085    pub fn backends(&self) -> &Backends {
1086        &self.backends
1087    }
1088
1089    /// Return the timing information from the most recent successful [`update`](Self::update) call.
1090    ///
1091    /// Returns `None` if [`update`](Self::update) has never completed successfully.
1092    pub fn last_update_timing(&self) -> Option<UpdateTimings> {
1093        **self.last_update_timing.load()
1094    }
1095}
1096
1097/// Timing information for one selector rebuild.
1098#[derive(Debug, Clone, Copy)]
1099pub struct SelectorUpdateTimings {
1100    /// The backend generation used to build the selector.
1101    pub generation: u64,
1102    /// Time from the discovery update until selector construction started.
1103    pub queue_duration: Duration,
1104    /// Time spent constructing and publishing the selector.
1105    pub build_duration: Duration,
1106}
1107
1108/// A request to rebuild one selector for a backend generation.
1109struct SelectorRebuildRequest {
1110    /// Backend generation this request will build.
1111    generation: u64,
1112    /// Backend membership captured for this generation.
1113    backends: Arc<BTreeSet<Backend>>,
1114    /// Readiness snapshot published with the rebuilt selector, kept together
1115    /// with membership and generation through coalescing, retries, and
1116    /// cancellation.
1117    readiness: ReadinessSnapshot,
1118    /// Time the rebuild was requested, used to measure queue delay.
1119    requested_at: Instant,
1120}
1121
1122/// Per-selector state for one active rebuild and its newest pending request.
1123#[derive(Default)]
1124struct SelectorRebuildState {
1125    /// Whether a worker currently owns this selector's rebuild loop.
1126    is_running: bool,
1127    /// Newest request waiting for that worker; older requests are replaced.
1128    pending_request: Option<SelectorRebuildRequest>,
1129}
1130
1131/// Restores a selector's rebuild state if its worker exits unexpectedly.
1132struct SelectorRebuildTaskGuard<S>
1133where
1134    S: BackendSelection,
1135{
1136    /// Selector slot whose worker state this guard owns.
1137    slot: Arc<SelectorSlot<S>>,
1138    /// Wakes the group background loop after cleanup.
1139    rebuild_notify: Arc<Notify>,
1140    /// Request restored to `pending` if the worker stops during a build.
1141    in_flight: Option<SelectorRebuildRequest>,
1142    /// Whether dropping this guard should perform cleanup.
1143    armed: bool,
1144}
1145
1146/// Monotonic cancellation signal shared by all selector tasks in one group.
1147///
1148/// [`Self::wait_for_cancel`] registers its waiter before rechecking the flag because
1149/// [`Notify::notify_waiters`] does not retain a permit for future waiters.
1150struct SelectorRebuildCancellation {
1151    /// Remains true after cancellation begins.
1152    cancelled: AtomicBool,
1153    /// Wakes every rebuild task waiting for cancellation.
1154    notify: Notify,
1155}
1156
1157impl SelectorRebuildCancellation {
1158    fn new() -> Self {
1159        Self {
1160            cancelled: AtomicBool::new(false),
1161            notify: Notify::new(),
1162        }
1163    }
1164
1165    fn cancel(&self) {
1166        self.cancelled.store(true, Release);
1167        self.notify.notify_waiters();
1168    }
1169
1170    fn is_cancelled(&self) -> bool {
1171        self.cancelled.load(Acquire)
1172    }
1173
1174    async fn wait_for_cancel(&self) {
1175        if self.is_cancelled() {
1176            return;
1177        }
1178
1179        let notified = self.notify.notified();
1180        tokio::pin!(notified);
1181        notified.as_mut().enable();
1182        if self.is_cancelled() {
1183            return;
1184        }
1185        notified.await;
1186    }
1187}
1188
1189impl<S> SelectorRebuildTaskGuard<S>
1190where
1191    S: BackendSelection,
1192{
1193    fn new(slot: Arc<SelectorSlot<S>>, rebuild_notify: Arc<Notify>) -> Self {
1194        Self {
1195            slot,
1196            rebuild_notify,
1197            in_flight: None,
1198            armed: true,
1199        }
1200    }
1201
1202    fn disarm(&mut self) {
1203        self.armed = false;
1204    }
1205}
1206
1207impl<S> Drop for SelectorRebuildTaskGuard<S>
1208where
1209    S: BackendSelection,
1210{
1211    fn drop(&mut self) {
1212        // Normal completion disarms the guard after clearing the worker state.
1213        if !self.armed {
1214            return;
1215        }
1216
1217        // Record a worker panic that bypassed the normal rebuild error path.
1218        if std::thread::panicking() {
1219            self.slot.failed_rebuilds.fetch_add(1, Relaxed);
1220            log::error!("load-balancing selector rebuild task panicked");
1221        }
1222        // This lock is never held across an await or user code, so task
1223        // cancellation cannot re-enter it and competing critical sections are short.
1224        let mut rebuild_state = lock_rebuild_state(&self.slot.rebuild_state);
1225        if let Some(request) = self.in_flight.take() {
1226            if rebuild_state
1227                .pending_request
1228                .as_ref()
1229                .is_none_or(|pending| pending.generation <= request.generation)
1230            {
1231                rebuild_state.pending_request = Some(request);
1232            }
1233        }
1234        // no worker running the rebuild anymore
1235        rebuild_state.is_running = false;
1236        drop(rebuild_state);
1237        // Prompt startup readiness to recheck generations after unexpected
1238        // cleanup. This does not restart the stopped rebuild worker.
1239        self.rebuild_notify.notify_one();
1240    }
1241}
1242
1243/// One independently configured selector in a [`LoadBalancerGroup`].
1244struct SelectorSlot<S>
1245where
1246    S: BackendSelection,
1247{
1248    /// Selector snapshot currently used by requests.
1249    selector: ArcSwap<PublishedSelector<S>>,
1250    /// Configuration used to build this selector.
1251    config: Option<S::Config>,
1252    /// Backend generation served by the published selector.
1253    generation: AtomicU64,
1254    /// Active worker and newest pending rebuild request.
1255    rebuild_state: Mutex<SelectorRebuildState>,
1256    /// Timing from the most recently published rebuild.
1257    last_update_timing: ArcSwap<Option<SelectorUpdateTimings>>,
1258    /// Number of pending requests replaced by newer generations.
1259    coalesced_rebuilds: AtomicU64,
1260    /// Number of rebuild attempts that failed.
1261    failed_rebuilds: AtomicU64,
1262    /// Interrupts retry backoff when a newer request arrives.
1263    interrupt_retry: Notify,
1264}
1265
1266/// Signals when a retired selector and all of its readers have been released.
1267struct SelectorReleaseSignal {
1268    /// Whether the retired selector and all readers have been dropped.
1269    released: AtomicBool,
1270    /// Wakes the rebuild gate when release completes.
1271    notify: Notify,
1272}
1273
1274impl SelectorReleaseSignal {
1275    fn new() -> Self {
1276        Self {
1277            released: AtomicBool::new(false),
1278            notify: Notify::new(),
1279        }
1280    }
1281
1282    async fn wait_for_release(&self) {
1283        loop {
1284            let notified = self.notify.notified();
1285            tokio::pin!(notified);
1286            notified.as_mut().enable();
1287            if self.released.load(Acquire) {
1288                return;
1289            }
1290            notified.await;
1291        }
1292    }
1293}
1294
1295/// Field-drop guard that signals a selector's release.
1296///
1297/// Kept as the final field of [`PublishedSelector`] so its [`Drop`] runs
1298/// *after* the `Arc<S>` selector field has been dropped. A manual
1299/// `impl Drop for PublishedSelector` would instead run before any field is
1300/// dropped, signaling release while the selector is still alive and violating
1301/// the one-additional-generation memory bound.
1302struct SelectorReleaseGuard {
1303    /// Signal updated when this final field is dropped.
1304    release_signal: Arc<SelectorReleaseSignal>,
1305}
1306
1307impl Drop for SelectorReleaseGuard {
1308    fn drop(&mut self) {
1309        self.release_signal.released.store(true, Release);
1310        self.release_signal.notify.notify_one();
1311    }
1312}
1313
1314/// A built selector snapshot published to request readers through [`ArcSwap`].
1315///
1316/// It pairs the selection data with the readiness snapshot for its generation
1317/// and a signal used to track when a replaced snapshot is no longer referenced.
1318/// Keeping readiness here lets a retired selector keep serving its own backends
1319/// with their own readiness, independently of the current view state.
1320struct PublishedSelector<S> {
1321    /// Declared before `release_guard` so the shared selector is dropped (and,
1322    /// when this holds the last reference, destroyed) before release is
1323    /// signaled.
1324    selector: Arc<S>,
1325    /// Readiness for the generation this selector was built from.
1326    readiness: ReadinessSnapshot,
1327    /// Signals after `selector` and all published references are dropped.
1328    release_guard: SelectorReleaseGuard,
1329}
1330
1331impl<S> PublishedSelector<S> {
1332    fn new(selector: S, readiness: ReadinessSnapshot) -> Self {
1333        Self {
1334            selector: Arc::new(selector),
1335            readiness,
1336            release_guard: SelectorReleaseGuard {
1337                release_signal: Arc::new(SelectorReleaseSignal::new()),
1338            },
1339        }
1340    }
1341
1342    fn release_signal(&self) -> Arc<SelectorReleaseSignal> {
1343        Arc::clone(&self.release_guard.release_signal)
1344    }
1345}
1346
1347/// A serial rebuild gate shared by one or more [`LoadBalancerGroup`]s.
1348///
1349/// The gate permits one selector build at a time. After a replacement is
1350/// published, it also waits for all readers of the retired selector to release
1351/// it before permitting another build. Groups that share one gate therefore
1352/// retain at most one additional selector generation beyond their currently
1353/// published selectors: either one replacement under construction or one
1354/// retired selector still held by readers, never both.
1355///
1356/// Retired selectors notify the gate when their final reader releases them, so
1357/// one gate can coordinate groups with different selector implementations
1358/// without polling.
1359pub struct SelectorRebuildGate {
1360    /// Allows only one selector build through this gate at a time.
1361    semaphore: Arc<Semaphore>,
1362    /// Release signal that must complete before the next build starts.
1363    retired: Mutex<Option<Arc<SelectorReleaseSignal>>>,
1364}
1365
1366impl Default for SelectorRebuildGate {
1367    fn default() -> Self {
1368        Self::new()
1369    }
1370}
1371
1372impl SelectorRebuildGate {
1373    /// Create a serial selector rebuild gate.
1374    pub fn new() -> Self {
1375        Self {
1376            semaphore: Arc::new(Semaphore::new(1)),
1377            retired: Mutex::new(None),
1378        }
1379    }
1380
1381    async fn acquire(self: &Arc<Self>) -> OwnedSemaphorePermit {
1382        let permit = Arc::clone(&self.semaphore)
1383            .acquire_owned()
1384            .await
1385            .expect("selector rebuild gate semaphore is never closed");
1386        let retired = self
1387            .retired
1388            .lock()
1389            .unwrap_or_else(|poisoned| poisoned.into_inner())
1390            .clone();
1391        if let Some(retired) = retired {
1392            retired.wait_for_release().await;
1393            let mut registered = self
1394                .retired
1395                .lock()
1396                .unwrap_or_else(|poisoned| poisoned.into_inner());
1397            // Clear only the signal we waited for, not a newer retired selector.
1398            if registered
1399                .as_ref()
1400                .is_some_and(|current| Arc::ptr_eq(current, &retired))
1401            {
1402                registered.take();
1403            }
1404        }
1405        permit
1406    }
1407
1408    /// Register `selector` as the generation that must be released before the
1409    /// next rebuild can start.
1410    ///
1411    /// The caller must hold the permit returned by [`Self::acquire`] and call
1412    /// this before dropping it. Otherwise another rebuild could acquire the
1413    /// gate without observing the retired selector's release signal.
1414    fn retire<S>(&self, selector: &Arc<PublishedSelector<S>>) {
1415        let release_signal = selector.release_signal();
1416        *self
1417            .retired
1418            .lock()
1419            .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(release_signal);
1420    }
1421}
1422
1423/// A collection of eventually consistent load-balancing selectors that share
1424/// one backend pool.
1425///
1426/// Discovery membership and readiness are published independently from
1427/// selector construction. Each selector continues serving its previous
1428/// generation while its replacement is built on a blocking worker. Pending
1429/// updates are coalesced so that at most one newer generation waits behind an
1430/// in-progress build or a shared rebuild gate. Selector builds and retired
1431/// generations are bounded by a [`SelectorRebuildGate`].
1432///
1433/// Each published selector owns the readiness snapshot for its generation, so a
1434/// removed backend stays selectable only through selectors still serving an
1435/// older generation. That readiness is released when the selector is replaced
1436/// and its last reader drops it; no separate pruning step is needed.
1437pub struct LoadBalancerGroup<S>
1438where
1439    S: BackendSelection,
1440{
1441    /// Backend membership and readiness shared by every selector.
1442    backends: Backends,
1443    /// Independently configured selectors and their rebuild state.
1444    selectors: Box<[Arc<SelectorSlot<S>>]>,
1445    /// Limits selector builds and waits for retired selectors to be released.
1446    rebuild_gate: Arc<SelectorRebuildGate>,
1447    /// Wakes startup readiness checks when a selector rebuild changes state.
1448    rebuild_notify: Arc<Notify>,
1449    /// Stops selector rebuild tasks when the group is dropped.
1450    rebuild_cancellation: Arc<SelectorRebuildCancellation>,
1451
1452    /// Timing information from the most recent [`update`](Self::update) call.
1453    ///
1454    /// `None` until the first successful update completes.
1455    last_update_timing: ArcSwap<Option<UpdateTimings>>,
1456
1457    /// How frequently the health check logic (if set) should run.
1458    ///
1459    /// If `None`, the health check logic will only run once at the beginning.
1460    /// This setting is ignored for views created with
1461    /// [`BackendView::new_with_health_registry`]; use [`HealthCheckService`]
1462    /// for those views.
1463    pub health_check_frequency: Option<Duration>,
1464    /// How frequently service discovery should run.
1465    ///
1466    /// If `None`, service discovery will only run once at the beginning.
1467    pub update_frequency: Option<Duration>,
1468    /// Whether to run health checks for all backends in parallel. Default is false.
1469    pub parallel_health_check: bool,
1470}
1471
1472fn lock_rebuild_state(state: &Mutex<SelectorRebuildState>) -> MutexGuard<'_, SelectorRebuildState> {
1473    state
1474        .lock()
1475        .unwrap_or_else(|poisoned| poisoned.into_inner())
1476}
1477
1478/// Initial delay before retrying a failed selector rebuild.
1479///
1480/// Failed builds (e.g. a panicking selector constructor) are retried so a
1481/// transient failure cannot strand a selector at a stale generation when
1482/// discovery membership does not change again. The delay backs off
1483/// exponentially up to [`MAX_SELECTOR_REBUILD_BACKOFF`] to bound retries and
1484/// avoid a hot loop when a build fails persistently.
1485const INITIAL_SELECTOR_REBUILD_BACKOFF: Duration = Duration::from_millis(10);
1486/// Upper bound for the retry delay after repeated selector rebuild failures.
1487const MAX_SELECTOR_REBUILD_BACKOFF: Duration = Duration::from_secs(1);
1488
1489async fn run_selector_rebuilds<S>(
1490    slot: Arc<SelectorSlot<S>>,
1491    rebuild_gate: Arc<SelectorRebuildGate>,
1492    rebuild_notify: Arc<Notify>,
1493    rebuild_cancellation: Arc<SelectorRebuildCancellation>,
1494) where
1495    S: BackendSelection + Send + Sync + 'static,
1496    S::Config: 'static,
1497{
1498    // This future can be dropped at any await. The guard restores an in-flight
1499    // request and clears `running` so a later rebuild can start a new worker.
1500    let mut task_guard =
1501        SelectorRebuildTaskGuard::new(Arc::clone(&slot), Arc::clone(&rebuild_notify));
1502    let mut backoff = INITIAL_SELECTOR_REBUILD_BACKOFF;
1503    loop {
1504        if rebuild_cancellation.is_cancelled() {
1505            // rebuild cancelled already, exit
1506            return;
1507        }
1508        {
1509            let mut rebuild_state = lock_rebuild_state(&slot.rebuild_state);
1510            if rebuild_state.pending_request.is_none() {
1511                // nothing to do, exit
1512                rebuild_state.is_running = false;
1513                task_guard.disarm();
1514                return;
1515            }
1516        }
1517
1518        let permit = tokio::select! {
1519            // wait for exclusive rebuild access and for readers to release the retired selector
1520            permit = rebuild_gate.acquire() => permit,
1521            _ = rebuild_cancellation.wait_for_cancel() => return, // cancelled
1522        };
1523
1524        let Some(request) = lock_rebuild_state(&slot.rebuild_state)
1525            .pending_request
1526            .take()
1527        else {
1528            // another worker took the job already
1529            drop(permit);
1530            continue;
1531        };
1532        // consume the wakeup associated with the request now being processed
1533        // so we do not incorrectly interrupt a later retry
1534        let _ = slot.interrupt_retry.notified().now_or_never();
1535        let build_start = Instant::now();
1536        let queue_duration = build_start.saturating_duration_since(request.requested_at);
1537        let generation = request.generation;
1538        let requested_at = request.requested_at;
1539        let backends = Arc::clone(&request.backends);
1540        let readiness = request.readiness.clone();
1541        task_guard.in_flight = Some(request);
1542        let build_backends = Arc::clone(&backends);
1543        let build_slot = Arc::clone(&slot);
1544        let result = tokio::task::spawn_blocking(move || {
1545            build_selector::<S>(&build_backends, build_slot.config.as_ref())
1546        })
1547        .await;
1548        let build_duration = build_start.elapsed();
1549
1550        if rebuild_cancellation.is_cancelled() {
1551            // cancelled during construction
1552            if let Ok(selector) = result {
1553                let _drop_task = tokio::task::spawn_blocking(move || {
1554                    drop(selector);
1555                    drop(permit);
1556                });
1557            } else {
1558                drop(permit);
1559            }
1560            return;
1561        }
1562
1563        match result {
1564            Ok(selector) => {
1565                let retired = slot
1566                    .selector
1567                    .swap(Arc::new(PublishedSelector::new(selector, readiness)));
1568                // Track the replaced selector until all readers release it.
1569                rebuild_gate.retire(&retired);
1570                let _drop_task = tokio::task::spawn_blocking(move || drop(retired));
1571                slot.last_update_timing
1572                    .store(Arc::new(Some(SelectorUpdateTimings {
1573                        generation,
1574                        queue_duration,
1575                        build_duration,
1576                    })));
1577                slot.generation.store(generation, Release);
1578                task_guard.in_flight = None;
1579                backoff = INITIAL_SELECTOR_REBUILD_BACKOFF;
1580                drop(permit);
1581                // notify load balancer group
1582                rebuild_notify.notify_one();
1583            }
1584            Err(error) => {
1585                slot.failed_rebuilds.fetch_add(1, Relaxed);
1586                if error.is_panic() {
1587                    log::error!(
1588                        "load-balancing selector rebuild panicked for generation {generation}: {error}"
1589                    );
1590                } else if error.is_cancelled() {
1591                    log::error!(
1592                        "load-balancing selector rebuild was cancelled for generation {generation}: {error}"
1593                    );
1594                } else {
1595                    log::error!(
1596                        "load-balancing selector rebuild failed for generation {generation}: {error}"
1597                    );
1598                }
1599                // Reschedule this generation unless a newer request already
1600                // superseded it, then release the gate and retry after a
1601                // bounded backoff so the selector can still converge even when
1602                // discovery membership does not change again.
1603                let retry_failed_generation = {
1604                    let mut state = lock_rebuild_state(&slot.rebuild_state);
1605                    if state
1606                        .pending_request
1607                        .as_ref()
1608                        .is_some_and(|pending| pending.generation > generation)
1609                    {
1610                        false
1611                    } else {
1612                        state.pending_request = Some(SelectorRebuildRequest {
1613                            generation,
1614                            backends,
1615                            readiness,
1616                            requested_at,
1617                        });
1618                        true
1619                    }
1620                };
1621                task_guard.in_flight = None;
1622                drop(permit);
1623                if retry_failed_generation {
1624                    tokio::select! {
1625                        _ = tokio::time::sleep(backoff) => {
1626                            // The failed generation still needs retrying; increase
1627                            // the delay if its next attempt also fails.
1628                            backoff = (backoff * 2).min(MAX_SELECTOR_REBUILD_BACKOFF);
1629                        }
1630                        _ = slot.interrupt_retry.notified() => {
1631                            // A newer generation is pending, so process it now
1632                            // instead of waiting on the older failure's backoff.
1633                            backoff = INITIAL_SELECTOR_REBUILD_BACKOFF;
1634                        }
1635                        // The group was dropped while this worker was waiting.
1636                        _ = rebuild_cancellation.wait_for_cancel() => return,
1637                    }
1638                } else {
1639                    // Consume the notification associated with the pending newer
1640                    // generation so it cannot skip a later retry delay.
1641                    let _ = slot.interrupt_retry.notified().now_or_never();
1642                    backoff = INITIAL_SELECTOR_REBUILD_BACKOFF;
1643                }
1644            }
1645        }
1646    }
1647}
1648
1649impl<S> LoadBalancerGroup<S>
1650where
1651    S: BackendSelection + Send + Sync + 'static,
1652    S::Config: 'static,
1653    S::Iter: BackendIter,
1654{
1655    /// Build a group of selectors over one shared backend pool.
1656    ///
1657    /// Each item in `configs` creates one selector. `None` uses
1658    /// [`BackendSelection::build`], while `Some(config)` uses
1659    /// [`BackendSelection::build_with_config`].
1660    ///
1661    /// # Panics
1662    ///
1663    /// Panics if `backends` has already been updated. A group owns backend
1664    /// updates and must start with selector generation zero.
1665    pub fn from_backends_with_configs(
1666        backends: Backends,
1667        configs: impl IntoIterator<Item = Option<S::Config>>,
1668    ) -> Self {
1669        assert_eq!(
1670            backends.generation(),
1671            0,
1672            "backends must not be updated before constructing a load balancer group"
1673        );
1674        let current_backends = backends.get_backend();
1675        let current_readiness = backends.readiness_snapshot();
1676        let selectors = configs
1677            .into_iter()
1678            .map(|config| {
1679                Arc::new(SelectorSlot {
1680                    selector: ArcSwap::new(Arc::new(PublishedSelector::new(
1681                        build_selector::<S>(&current_backends, config.as_ref()),
1682                        current_readiness.clone(),
1683                    ))),
1684                    config,
1685                    generation: AtomicU64::new(0),
1686                    rebuild_state: Mutex::new(SelectorRebuildState::default()),
1687                    last_update_timing: ArcSwap::new(Arc::new(None)),
1688                    coalesced_rebuilds: AtomicU64::new(0),
1689                    failed_rebuilds: AtomicU64::new(0),
1690                    interrupt_retry: Notify::new(),
1691                })
1692            })
1693            .collect();
1694
1695        Self {
1696            backends,
1697            selectors,
1698            rebuild_gate: Arc::new(SelectorRebuildGate::new()),
1699            rebuild_notify: Arc::new(Notify::new()),
1700            rebuild_cancellation: Arc::new(SelectorRebuildCancellation::new()),
1701            last_update_timing: ArcSwap::new(Arc::new(None)),
1702            health_check_frequency: None,
1703            update_frequency: None,
1704            parallel_health_check: false,
1705        }
1706    }
1707
1708    /// Use a rebuild gate shared with other selector groups.
1709    ///
1710    /// A group uses a private serial gate by default. Supplying the same gate
1711    /// to multiple groups extends the one-extra-generation memory bound across
1712    /// all of them.
1713    pub fn with_rebuild_gate(mut self, rebuild_gate: Arc<SelectorRebuildGate>) -> Self {
1714        self.rebuild_gate = rebuild_gate;
1715        self
1716    }
1717
1718    /// Return the number of selectors in this group.
1719    pub fn selector_count(&self) -> usize {
1720        self.selectors.len()
1721    }
1722
1723    fn schedule_selector_rebuild(
1724        &self,
1725        slot: &Arc<SelectorSlot<S>>,
1726        update: &BackendUpdate,
1727        requested_at: Instant,
1728    ) {
1729        let should_spawn = {
1730            let mut rebuild_state = lock_rebuild_state(&slot.rebuild_state);
1731            // update selector rebuild request if another one happens while we were already pending
1732            if rebuild_state
1733                .pending_request
1734                .replace(SelectorRebuildRequest {
1735                    generation: update.generation,
1736                    backends: Arc::clone(&update.backends),
1737                    readiness: update.readiness.clone(),
1738                    requested_at,
1739                })
1740                .is_some()
1741            {
1742                slot.coalesced_rebuilds.fetch_add(1, Relaxed);
1743            }
1744
1745            if rebuild_state.is_running {
1746                // already spawned and running
1747                false
1748            } else {
1749                // spawn it
1750                rebuild_state.is_running = true;
1751                true
1752            }
1753        };
1754
1755        if should_spawn {
1756            let task = run_selector_rebuilds(
1757                Arc::clone(slot),
1758                Arc::clone(&self.rebuild_gate),
1759                Arc::clone(&self.rebuild_notify),
1760                Arc::clone(&self.rebuild_cancellation),
1761            );
1762            let _rebuild_task = tokio::spawn(task);
1763        } else {
1764            slot.interrupt_retry.notify_one();
1765        }
1766    }
1767
1768    /// Run service discovery and enqueue selector rebuilds when membership changes.
1769    ///
1770    /// The discovered backend membership is published before this method returns.
1771    /// Selector rebuilds run asynchronously. A removed backend stays selectable
1772    /// through selectors still serving an older generation, via the readiness
1773    /// snapshot each of those selectors owns.
1774    ///
1775    /// To wait for convergence, read [`backend_generation`](Self::backend_generation)
1776    /// after this call and poll [`selectors_ready_for`](Self::selectors_ready_for).
1777    /// Calls on the same group must not overlap. [`Self::run`] serializes them.
1778    pub async fn update(&self) -> Result<()> {
1779        let start = Instant::now();
1780        let changed = self.backends.update_backends().await?;
1781        let discovery_duration = start.elapsed();
1782
1783        if let Some(update) = changed {
1784            for slot in &self.selectors {
1785                self.schedule_selector_rebuild(slot, &update, Instant::now());
1786            }
1787        }
1788
1789        self.last_update_timing.store(Arc::new(Some(UpdateTimings {
1790            discovery_duration,
1791            // Selector builds continue asynchronously and report their own timing.
1792            build_duration: Duration::ZERO,
1793        })));
1794
1795        Ok(())
1796    }
1797
1798    /// Return the first healthy backend from the selected load-balancing configuration.
1799    ///
1800    /// Returns `None` when `selector_index` is out of bounds.
1801    pub fn select(
1802        &self,
1803        selector_index: usize,
1804        key: &[u8],
1805        max_iterations: usize,
1806    ) -> Option<Backend> {
1807        self.select_with(selector_index, key, max_iterations, |_, health| health)
1808    }
1809
1810    /// Select a backend using one selector and an additional acceptance function.
1811    ///
1812    /// Each selector consults the readiness snapshot published with it, so a
1813    /// selector serving an older generation keeps using that generation's
1814    /// readiness. Returns `None` when `selector_index` is out of bounds.
1815    pub fn select_with<F>(
1816        &self,
1817        selector_index: usize,
1818        key: &[u8],
1819        max_iterations: usize,
1820        accept: F,
1821    ) -> Option<Backend>
1822    where
1823        F: Fn(&Backend, bool) -> bool,
1824    {
1825        // `published` is an `ArcSwap` guard held for the whole selection so the
1826        // selector data and its matching readiness snapshot share one snapshot
1827        // without a per-request `Arc` clone.
1828        //
1829        // Declaration order matters for the release invariant: `published` is
1830        // declared before `iter`, so `iter` (and the per-iteration `Arc<S>`
1831        // clone it holds) is dropped before the guard. The guard's drop can
1832        // destroy the retired selector and fire its release signal, so release
1833        // must not be signaled while an iterator still references the selector.
1834        let published = self.selectors.get(selector_index)?.selector.load();
1835        let mut iter = UniqueIterator::new(published.selector.iter(key), max_iterations);
1836        while let Some(backend) = iter.get_next() {
1837            if accept(&backend, published.readiness.ready(&backend)) {
1838                return Some(backend);
1839            }
1840        }
1841        None
1842    }
1843
1844    /// Set the health check implementation shared by every selector.
1845    pub fn set_health_check(
1846        &mut self,
1847        hc: Box<dyn health_check::HealthCheck + Send + Sync + 'static>,
1848    ) {
1849        self.backends.set_health_check(hc);
1850    }
1851
1852    /// Access the shared backend pool.
1853    pub fn backends(&self) -> &Backends {
1854        &self.backends
1855    }
1856
1857    /// Return the latest backend membership generation.
1858    pub fn backend_generation(&self) -> u64 {
1859        self.backends.generation()
1860    }
1861
1862    /// Return the generation currently served by one selector.
1863    pub fn selector_generation(&self, selector_index: usize) -> Option<u64> {
1864        self.selectors
1865            .get(selector_index)
1866            .map(|slot| slot.generation.load(Acquire))
1867    }
1868
1869    /// Return whether every selector serves at least `generation`.
1870    pub fn selectors_ready_for(&self, generation: u64) -> bool {
1871        self.selectors
1872            .iter()
1873            .all(|slot| slot.generation.load(Acquire) >= generation)
1874    }
1875
1876    /// Return timing information for the most recently published selector generation.
1877    pub fn selector_last_update_timing(
1878        &self,
1879        selector_index: usize,
1880    ) -> Option<SelectorUpdateTimings> {
1881        self.selectors
1882            .get(selector_index)
1883            .and_then(|slot| **slot.last_update_timing.load())
1884    }
1885
1886    /// Return how many pending selector generations were replaced by a newer one.
1887    pub fn selector_coalesced_rebuilds(&self, selector_index: usize) -> Option<u64> {
1888        self.selectors
1889            .get(selector_index)
1890            .map(|slot| slot.coalesced_rebuilds.load(Relaxed))
1891    }
1892
1893    /// Return how many selector rebuild tasks failed.
1894    pub fn selector_failed_rebuilds(&self, selector_index: usize) -> Option<u64> {
1895        self.selectors
1896            .get(selector_index)
1897            .map(|slot| slot.failed_rebuilds.load(Relaxed))
1898    }
1899
1900    /// Wait for a selector rebuild to complete or terminate unexpectedly.
1901    pub(crate) fn rebuild_notified(&self) -> impl Future<Output = ()> + '_ {
1902        self.rebuild_notify.notified()
1903    }
1904
1905    /// Return timing information from the most recent successful [`update`](Self::update).
1906    pub fn last_update_timing(&self) -> Option<UpdateTimings> {
1907        **self.last_update_timing.load()
1908    }
1909}
1910
1911impl<S> Drop for LoadBalancerGroup<S>
1912where
1913    S: BackendSelection,
1914{
1915    fn drop(&mut self) {
1916        self.rebuild_cancellation.cancel();
1917    }
1918}
1919
1920#[cfg(test)]
1921mod test {
1922    use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed};
1923    use std::sync::Condvar;
1924
1925    use super::*;
1926    use async_trait::async_trait;
1927    use pingora_core::services::ServiceReadyNotifier;
1928
1929    struct BuildTracker {
1930        blocked: Mutex<bool>,
1931        unblocked: Condvar,
1932        /// Builds whose zero-based index is `>= block_from` block until
1933        /// released, independently of the `blocked` flag. This allows releasing
1934        /// an earlier build while deterministically holding a later one.
1935        block_from: AtomicUsize,
1936        /// Number of subsequent builds that should panic before succeeding.
1937        panics: AtomicUsize,
1938        /// Whether selector destructors should block until released.
1939        drop_blocked: Mutex<bool>,
1940        drop_unblocked: Condvar,
1941        drop_waiters: AtomicUsize,
1942        active: AtomicUsize,
1943        max_active: AtomicUsize,
1944        builds: AtomicUsize,
1945        live_selectors: AtomicUsize,
1946        max_live_selectors: AtomicUsize,
1947    }
1948
1949    impl Default for BuildTracker {
1950        fn default() -> Self {
1951            Self {
1952                blocked: Mutex::new(false),
1953                unblocked: Condvar::new(),
1954                block_from: AtomicUsize::new(usize::MAX),
1955                panics: AtomicUsize::new(0),
1956                drop_blocked: Mutex::new(false),
1957                drop_unblocked: Condvar::new(),
1958                drop_waiters: AtomicUsize::new(0),
1959                active: AtomicUsize::new(0),
1960                max_active: AtomicUsize::new(0),
1961                builds: AtomicUsize::new(0),
1962                live_selectors: AtomicUsize::new(0),
1963                max_live_selectors: AtomicUsize::new(0),
1964            }
1965        }
1966    }
1967
1968    impl BuildTracker {
1969        fn run_build(&self) {
1970            if self.panics.load(Relaxed) > 0 {
1971                self.panics.fetch_sub(1, Relaxed);
1972                panic!("intentional selector build panic");
1973            }
1974            let index = self.builds.fetch_add(1, Relaxed);
1975            let active = self.active.fetch_add(1, Relaxed) + 1;
1976            self.max_active.fetch_max(active, Relaxed);
1977
1978            let mut blocked = self
1979                .blocked
1980                .lock()
1981                .unwrap_or_else(|poisoned| poisoned.into_inner());
1982            while *blocked || index >= self.block_from.load(Relaxed) {
1983                blocked = self
1984                    .unblocked
1985                    .wait(blocked)
1986                    .unwrap_or_else(|poisoned| poisoned.into_inner());
1987            }
1988            self.active.fetch_sub(1, Relaxed);
1989        }
1990
1991        fn block(&self) {
1992            *self
1993                .blocked
1994                .lock()
1995                .unwrap_or_else(|poisoned| poisoned.into_inner()) = true;
1996        }
1997
1998        fn unblock(&self) {
1999            *self
2000                .blocked
2001                .lock()
2002                .unwrap_or_else(|poisoned| poisoned.into_inner()) = false;
2003            self.unblocked.notify_all();
2004        }
2005
2006        fn set_block_from(&self, index: usize) {
2007            self.block_from.store(index, Relaxed);
2008            self.unblocked.notify_all();
2009        }
2010
2011        fn set_panics(&self, count: usize) {
2012            self.panics.store(count, Relaxed);
2013        }
2014
2015        fn block_drop(&self) {
2016            *self
2017                .drop_blocked
2018                .lock()
2019                .unwrap_or_else(|poisoned| poisoned.into_inner()) = true;
2020        }
2021
2022        fn unblock_drop(&self) {
2023            *self
2024                .drop_blocked
2025                .lock()
2026                .unwrap_or_else(|poisoned| poisoned.into_inner()) = false;
2027            self.drop_unblocked.notify_all();
2028        }
2029
2030        fn wait_drop(&self) {
2031            self.drop_waiters.fetch_add(1, Relaxed);
2032            let mut blocked = self
2033                .drop_blocked
2034                .lock()
2035                .unwrap_or_else(|poisoned| poisoned.into_inner());
2036            while *blocked {
2037                blocked = self
2038                    .drop_unblocked
2039                    .wait(blocked)
2040                    .unwrap_or_else(|poisoned| poisoned.into_inner());
2041            }
2042            self.drop_waiters.fetch_sub(1, Relaxed);
2043        }
2044
2045        fn reset(&self) {
2046            self.active.store(0, Relaxed);
2047            self.max_active.store(0, Relaxed);
2048            self.builds.store(0, Relaxed);
2049            self.max_live_selectors
2050                .store(self.live_selectors.load(Relaxed), Relaxed);
2051        }
2052
2053        fn selector_created(&self) {
2054            let live = self.live_selectors.fetch_add(1, Relaxed) + 1;
2055            self.max_live_selectors.fetch_max(live, Relaxed);
2056        }
2057
2058        fn selector_dropped(&self) {
2059            self.live_selectors.fetch_sub(1, Relaxed);
2060        }
2061    }
2062
2063    #[derive(Clone)]
2064    struct TestSelectionConfig {
2065        reverse: bool,
2066        tracker: Option<Arc<BuildTracker>>,
2067    }
2068
2069    struct TestSelection {
2070        backends: Vec<Backend>,
2071        tracker: Option<Arc<BuildTracker>>,
2072    }
2073
2074    impl Drop for TestSelection {
2075        fn drop(&mut self) {
2076            if let Some(tracker) = &self.tracker {
2077                tracker.wait_drop();
2078                tracker.selector_dropped();
2079            }
2080        }
2081    }
2082
2083    struct TestSelectionIter {
2084        selection: Arc<TestSelection>,
2085        index: usize,
2086    }
2087
2088    impl BackendIter for TestSelectionIter {
2089        fn next(&mut self) -> Option<&Backend> {
2090            let backend = self.selection.backends.get(self.index);
2091            self.index += 1;
2092            backend
2093        }
2094    }
2095
2096    impl BackendSelection for TestSelection {
2097        type Iter = TestSelectionIter;
2098        type Config = TestSelectionConfig;
2099
2100        fn build_with_config(backends: &BTreeSet<Backend>, config: &Self::Config) -> Self {
2101            if let Some(tracker) = &config.tracker {
2102                tracker.run_build();
2103            }
2104            let mut backends: Vec<_> = backends.iter().cloned().collect();
2105            if config.reverse {
2106                backends.reverse();
2107            }
2108            if let Some(tracker) = &config.tracker {
2109                tracker.selector_created();
2110            }
2111            Self {
2112                backends,
2113                tracker: config.tracker.clone(),
2114            }
2115        }
2116
2117        fn build(backends: &BTreeSet<Backend>) -> Self {
2118            Self {
2119                backends: backends.iter().cloned().collect(),
2120                tracker: None,
2121            }
2122        }
2123
2124        fn iter(self: &Arc<Self>, _key: &[u8]) -> Self::Iter {
2125            TestSelectionIter {
2126                selection: self.clone(),
2127                index: 0,
2128            }
2129        }
2130    }
2131
2132    struct MutableDiscovery {
2133        backends: ArcSwap<BTreeSet<Backend>>,
2134        enablement: ArcSwap<HashMap<u64, bool>>,
2135    }
2136
2137    impl MutableDiscovery {
2138        fn new(backends: BTreeSet<Backend>) -> Self {
2139            Self {
2140                backends: ArcSwap::new(Arc::new(backends)),
2141                enablement: ArcSwap::new(Arc::new(HashMap::new())),
2142            }
2143        }
2144
2145        fn add(&self, backend: Backend) {
2146            let mut backends = BTreeSet::clone(&self.backends.load());
2147            backends.insert(backend);
2148            self.backends.store(Arc::new(backends));
2149        }
2150
2151        fn replace(&self, backends: BTreeSet<Backend>) {
2152            self.backends.store(Arc::new(backends));
2153        }
2154
2155        fn set_enabled(&self, backend: &Backend, enabled: bool) {
2156            let mut enablement = HashMap::clone(&self.enablement.load());
2157            enablement.insert(backend.hash_key(), enabled);
2158            self.enablement.store(Arc::new(enablement));
2159        }
2160    }
2161
2162    #[async_trait]
2163    impl ServiceDiscovery for Arc<MutableDiscovery> {
2164        async fn discover(&self) -> Result<(BTreeSet<Backend>, HashMap<u64, bool>)> {
2165            Ok((
2166                BTreeSet::clone(&self.backends.load()),
2167                HashMap::clone(&self.enablement.load()),
2168            ))
2169        }
2170    }
2171
2172    struct FailThenWaitDiscovery {
2173        backend: Backend,
2174        attempts: AtomicUsize,
2175        allow_success: Notify,
2176    }
2177
2178    #[async_trait]
2179    impl ServiceDiscovery for Arc<FailThenWaitDiscovery> {
2180        async fn discover(&self) -> Result<(BTreeSet<Backend>, HashMap<u64, bool>)> {
2181            let attempt = self.attempts.fetch_add(1, Relaxed);
2182            if attempt == 0 {
2183                return Err(pingora_error::Error::explain(
2184                    ErrorType::InternalError,
2185                    "intentional discovery failure",
2186                ));
2187            }
2188            if attempt == 1 {
2189                self.allow_success.notified().await;
2190            }
2191            Ok((BTreeSet::from([self.backend.clone()]), HashMap::new()))
2192        }
2193    }
2194
2195    struct CountingHealthCheck {
2196        checks: Arc<AtomicUsize>,
2197    }
2198
2199    #[async_trait]
2200    impl health_check::HealthCheck for CountingHealthCheck {
2201        async fn check(&self, _target: &Backend) -> Result<()> {
2202            self.checks.fetch_add(1, Relaxed);
2203            Ok(())
2204        }
2205
2206        fn health_threshold(&self, _success: bool) -> usize {
2207            1
2208        }
2209    }
2210
2211    fn address_health_key(backend: &Backend) -> u64 {
2212        let mut hasher = DefaultHasher::new();
2213        backend.addr.hash(&mut hasher);
2214        hasher.finish()
2215    }
2216
2217    struct SelectiveHealthCheck {
2218        checks: Arc<AtomicUsize>,
2219        unhealthy: Option<SocketAddr>,
2220    }
2221
2222    #[async_trait]
2223    impl health_check::HealthCheck for SelectiveHealthCheck {
2224        async fn check(&self, target: &Backend) -> Result<()> {
2225            self.checks.fetch_add(1, Relaxed);
2226            if self.unhealthy.as_ref() == Some(&target.addr) {
2227                Err(pingora_error::Error::new(ErrorType::InternalError))
2228            } else {
2229                Ok(())
2230            }
2231        }
2232
2233        fn health_threshold(&self, _success: bool) -> usize {
2234            1
2235        }
2236    }
2237
2238    /// Health check whose result depends on the backend's full identity (its
2239    /// weight), so two backends sharing an address can produce different
2240    /// results.
2241    struct WeightHealthCheck {
2242        checks: Arc<AtomicUsize>,
2243        unhealthy_weight: usize,
2244    }
2245
2246    #[async_trait]
2247    impl health_check::HealthCheck for WeightHealthCheck {
2248        async fn check(&self, target: &Backend) -> Result<()> {
2249            self.checks.fetch_add(1, Relaxed);
2250            if target.weight == self.unhealthy_weight {
2251                Err(pingora_error::Error::new(ErrorType::InternalError))
2252            } else {
2253                Ok(())
2254            }
2255        }
2256
2257        fn health_threshold(&self, _success: bool) -> usize {
2258            1
2259        }
2260    }
2261
2262    async fn wait_for_group_generation(group: &LoadBalancerGroup<TestSelection>, generation: u64) {
2263        tokio::time::timeout(Duration::from_secs(5), async {
2264            while !group.selectors_ready_for(generation) {
2265                tokio::time::sleep(Duration::from_millis(1)).await;
2266            }
2267        })
2268        .await
2269        .expect("selector rebuild timed out");
2270    }
2271
2272    async fn wait_for_active_builds(tracker: &BuildTracker, expected: usize) {
2273        tokio::time::timeout(Duration::from_secs(5), async {
2274            while tracker.active.load(Relaxed) != expected {
2275                tokio::time::sleep(Duration::from_millis(1)).await;
2276            }
2277        })
2278        .await
2279        .expect("selector builds did not start");
2280    }
2281
2282    async fn wait_for_builds_started(tracker: &BuildTracker, expected: usize) {
2283        tokio::time::timeout(Duration::from_secs(5), async {
2284            while tracker.builds.load(Relaxed) != expected {
2285                tokio::time::sleep(Duration::from_millis(1)).await;
2286            }
2287        })
2288        .await
2289        .expect("selector builds did not start");
2290    }
2291
2292    #[tokio::test]
2293    async fn test_static_backends() {
2294        let backends: LoadBalancer<selection::RoundRobin> =
2295            LoadBalancer::try_from_iter(["1.1.1.1:80", "1.0.0.1:80"]).unwrap();
2296
2297        let backend1 = Backend::new("1.1.1.1:80").unwrap();
2298        let backend2 = Backend::new("1.0.0.1:80").unwrap();
2299        let backend = backends.backends().get_backend();
2300        assert!(backend.contains(&backend1));
2301        assert!(backend.contains(&backend2));
2302    }
2303
2304    #[tokio::test]
2305    async fn test_backends() {
2306        let discovery = discovery::Static::default();
2307        let good1 = Backend::new("1.1.1.1:80").unwrap();
2308        discovery.add(good1.clone());
2309        let good2 = Backend::new("1.0.0.1:80").unwrap();
2310        discovery.add(good2.clone());
2311        let bad = Backend::new("127.0.0.1:79").unwrap();
2312        discovery.add(bad.clone());
2313
2314        let mut backends = Backends::new(Box::new(discovery));
2315        let check = health_check::TcpHealthCheck::new();
2316        backends.set_health_check(check);
2317
2318        // true: new backend discovered
2319        let updated = AtomicBool::new(false);
2320        backends
2321            .update(|_| updated.store(true, Relaxed))
2322            .await
2323            .unwrap();
2324        assert!(updated.load(Relaxed));
2325
2326        // false: no new backend discovered
2327        let updated = AtomicBool::new(false);
2328        backends
2329            .update(|_| updated.store(true, Relaxed))
2330            .await
2331            .unwrap();
2332        assert!(!updated.load(Relaxed));
2333
2334        backends.run_health_check(false).await;
2335
2336        let backend = backends.get_backend();
2337        assert!(backend.contains(&good1));
2338        assert!(backend.contains(&good2));
2339        assert!(backend.contains(&bad));
2340
2341        assert!(backends.ready(&good1));
2342        assert!(backends.ready(&good2));
2343        assert!(!backends.ready(&bad));
2344    }
2345    #[tokio::test]
2346    async fn test_backends_with_ext() {
2347        let discovery = discovery::Static::default();
2348        let mut b1 = Backend::new("1.1.1.1:80").unwrap();
2349        b1.ext.insert(true);
2350        let mut b2 = Backend::new("1.0.0.1:80").unwrap();
2351        b2.ext.insert(1u8);
2352        discovery.add(b1.clone());
2353        discovery.add(b2.clone());
2354
2355        let backends = Backends::new(Box::new(discovery));
2356
2357        // fill in the backends
2358        backends.update(|_| {}).await.unwrap();
2359
2360        let backend = backends.get_backend();
2361        assert!(backend.contains(&b1));
2362        assert!(backend.contains(&b2));
2363
2364        let b2 = backend.first().unwrap();
2365        assert_eq!(b2.ext.get::<u8>(), Some(&1));
2366
2367        let b1 = backend.last().unwrap();
2368        assert_eq!(b1.ext.get::<bool>(), Some(&true));
2369    }
2370
2371    #[tokio::test]
2372    async fn test_discovery_readiness() {
2373        use discovery::Static;
2374
2375        struct TestDiscovery(Static);
2376        #[async_trait]
2377        impl ServiceDiscovery for TestDiscovery {
2378            async fn discover(&self) -> Result<(BTreeSet<Backend>, HashMap<u64, bool>)> {
2379                let bad = Backend::new("127.0.0.1:79").unwrap();
2380                let (backends, mut readiness) = self.0.discover().await?;
2381                readiness.insert(bad.hash_key(), false);
2382                Ok((backends, readiness))
2383            }
2384        }
2385        let discovery = Static::default();
2386        let good1 = Backend::new("1.1.1.1:80").unwrap();
2387        discovery.add(good1.clone());
2388        let good2 = Backend::new("1.0.0.1:80").unwrap();
2389        discovery.add(good2.clone());
2390        let bad = Backend::new("127.0.0.1:79").unwrap();
2391        discovery.add(bad.clone());
2392        let discovery = TestDiscovery(discovery);
2393
2394        let backends = Backends::new(Box::new(discovery));
2395
2396        // true: new backend discovered
2397        let updated = AtomicBool::new(false);
2398        backends
2399            .update(|_| updated.store(true, Relaxed))
2400            .await
2401            .unwrap();
2402        assert!(updated.load(Relaxed));
2403
2404        let backend = backends.get_backend();
2405        assert!(backend.contains(&good1));
2406        assert!(backend.contains(&good2));
2407        assert!(backend.contains(&bad));
2408
2409        assert!(backends.ready(&good1));
2410        assert!(backends.ready(&good2));
2411        assert!(!backends.ready(&bad));
2412    }
2413
2414    #[tokio::test]
2415    async fn test_parallel_health_check() {
2416        let discovery = discovery::Static::default();
2417        let good1 = Backend::new("1.1.1.1:80").unwrap();
2418        discovery.add(good1.clone());
2419        let good2 = Backend::new("1.0.0.1:80").unwrap();
2420        discovery.add(good2.clone());
2421        let bad = Backend::new("127.0.0.1:79").unwrap();
2422        discovery.add(bad.clone());
2423
2424        let mut backends = Backends::new(Box::new(discovery));
2425        let check = health_check::TcpHealthCheck::new();
2426        backends.set_health_check(check);
2427
2428        // true: new backend discovered
2429        let updated = AtomicBool::new(false);
2430        backends
2431            .update(|_| updated.store(true, Relaxed))
2432            .await
2433            .unwrap();
2434        assert!(updated.load(Relaxed));
2435
2436        backends.run_health_check(true).await;
2437
2438        assert!(backends.ready(&good1));
2439        assert!(backends.ready(&good2));
2440        assert!(!backends.ready(&bad));
2441    }
2442
2443    /// Health check that panics for one backend and, for every other backend,
2444    /// blocks on `release` before reporting a failure.
2445    ///
2446    /// Signalling `panicked` before the panic lets a test hold the other check
2447    /// in flight until the pass has already had to handle the panic.
2448    struct PanickingHealthCheck {
2449        panic_addr: SocketAddr,
2450        panicked: Arc<Notify>,
2451        release: Arc<Notify>,
2452    }
2453
2454    #[async_trait]
2455    impl health_check::HealthCheck for PanickingHealthCheck {
2456        async fn check(&self, target: &Backend) -> Result<()> {
2457            if target.addr == self.panic_addr {
2458                self.panicked.notify_one();
2459                panic!("intentional health check panic");
2460            }
2461            self.release.notified().await;
2462            Err(pingora_error::Error::new(ErrorType::InternalError))
2463        }
2464
2465        fn health_threshold(&self, _success: bool) -> usize {
2466            1
2467        }
2468    }
2469
2470    #[tokio::test]
2471    async fn test_parallel_health_check_isolates_a_panicking_check() {
2472        let discovery = discovery::Static::default();
2473        let panics = Backend::new("127.0.0.1:79").unwrap();
2474        let reports = Backend::new("1.1.1.1:80").unwrap();
2475        discovery.add(panics.clone());
2476        discovery.add(reports.clone());
2477
2478        let panicked = Arc::new(Notify::new());
2479        let release = Arc::new(Notify::new());
2480        let mut backends = Backends::new(Box::new(discovery));
2481        backends.set_health_check(Box::new(PanickingHealthCheck {
2482            panic_addr: panics.addr.clone(),
2483            panicked: Arc::clone(&panicked),
2484            release: Arc::clone(&release),
2485        }));
2486        backends.update(|_| {}).await.unwrap();
2487        let backends = Arc::new(backends);
2488
2489        // Backends start out healthy, so the flip asserted below can only have
2490        // come from this pass.
2491        assert!(backends.ready(&reports));
2492
2493        let pass = tokio::spawn({
2494            let backends = Arc::clone(&backends);
2495            async move { backends.run_health_check(true).await }
2496        });
2497
2498        // Release the other check only once the panic has happened, so the pass
2499        // has to carry a still-unfinished check past it.
2500        panicked.notified().await;
2501        release.notify_one();
2502
2503        // Expect the panic message in the test output: the panicking task's
2504        // `JoinError` is discarded here rather than resumed in the pass.
2505        tokio::time::timeout(Duration::from_secs(5), pass)
2506            .await
2507            .expect("a panicking check must not stall the pass")
2508            .expect("a panicking check must not be resumed in the pass");
2509
2510        assert!(
2511            !backends.ready(&reports),
2512            "the other backend's panic cancelled this check before it was reported"
2513        );
2514    }
2515
2516    #[tokio::test]
2517    async fn test_lb_update_stores_timing() {
2518        let discovery = discovery::Static::default();
2519        let b1 = Backend::new("1.1.1.1:80").unwrap();
2520        let b2 = Backend::new("1.0.0.1:80").unwrap();
2521        discovery.add(b1.clone());
2522        discovery.add(b2.clone());
2523
2524        let lb = LoadBalancer::<selection::RoundRobin>::from_backends(Backends::new(Box::new(
2525            discovery,
2526        )));
2527
2528        // Before first update, timing should be None
2529        assert!(lb.last_update_timing().is_none());
2530
2531        lb.update().await.unwrap();
2532
2533        // After update, timing should be populated
2534        let timing = lb
2535            .last_update_timing()
2536            .expect("timing should be Some after update");
2537        assert!(timing.discovery_duration > Duration::ZERO);
2538        assert!(timing.build_duration > Duration::ZERO);
2539
2540        // Backends should be populated
2541        let backend = lb.backends().get_backend();
2542        assert!(backend.contains(&b1));
2543        assert!(backend.contains(&b2));
2544
2545        // Selection should work
2546        assert!(lb.select(b"test", 10).is_some());
2547    }
2548
2549    #[tokio::test]
2550    #[should_panic(
2551        expected = "backends must not be updated before constructing a load balancer group"
2552    )]
2553    async fn test_load_balancer_group_rejects_updated_backends() {
2554        let backend = Backend::new("1.0.0.1:80").unwrap();
2555        let backends = Backends::new(Box::new(Arc::new(MutableDiscovery::new(BTreeSet::from([
2556            backend,
2557        ])))));
2558        backends.update(|_| {}).await.unwrap();
2559
2560        let _ = LoadBalancerGroup::<TestSelection>::from_backends_with_configs(backends, [None]);
2561    }
2562
2563    #[tokio::test]
2564    async fn test_load_balancer_group_rebuilds_all_selectors() {
2565        let b1 = Backend::new("1.0.0.1:80").unwrap();
2566        let b2 = Backend::new("1.1.1.1:80").unwrap();
2567        let b3 = Backend::new("1.1.1.2:80").unwrap();
2568        let discovery = Arc::new(MutableDiscovery::new(BTreeSet::from([
2569            b1.clone(),
2570            b2.clone(),
2571        ])));
2572
2573        let group = LoadBalancerGroup::<TestSelection>::from_backends_with_configs(
2574            Backends::new(Box::new(discovery.clone())),
2575            [
2576                Some(TestSelectionConfig {
2577                    reverse: false,
2578                    tracker: None,
2579                }),
2580                Some(TestSelectionConfig {
2581                    reverse: true,
2582                    tracker: None,
2583                }),
2584            ],
2585        );
2586
2587        assert_eq!(group.selector_count(), 2);
2588        group.update().await.unwrap();
2589        wait_for_group_generation(&group, 1).await;
2590        assert_eq!(group.select(0, b"", 10), Some(b1.clone()));
2591        assert_eq!(group.select(1, b"", 10), Some(b2));
2592
2593        discovery.add(b3.clone());
2594        group.update().await.unwrap();
2595        wait_for_group_generation(&group, 2).await;
2596        assert_eq!(group.select(0, b"", 10), Some(b1));
2597        assert_eq!(group.select(1, b"", 10), Some(b3));
2598        assert!(group.last_update_timing().is_some());
2599        assert_eq!(group.backend_generation(), 2);
2600        assert_eq!(group.selector_generation(0), Some(2));
2601        assert_eq!(group.select(2, b"", 10), None);
2602    }
2603
2604    #[tokio::test]
2605    async fn test_unchanged_backends_do_not_rebuild_view_or_selector() {
2606        let backend = Backend::new("1.0.0.1:80").unwrap();
2607        let discovery = Arc::new(MutableDiscovery::new(BTreeSet::from([backend])));
2608        let tracker = Arc::new(BuildTracker::default());
2609        let group = LoadBalancerGroup::<TestSelection>::from_backends_with_configs(
2610            Backends::new(Box::new(discovery)),
2611            [Some(TestSelectionConfig {
2612                reverse: false,
2613                tracker: Some(Arc::clone(&tracker)),
2614            })],
2615        );
2616        tracker.reset();
2617
2618        group.update().await.unwrap();
2619        wait_for_group_generation(&group, 1).await;
2620        let view_state = group.backends.state.load_full();
2621        let registry_state = group.backends.health_registry.state.load_full();
2622        let selector = group.selectors[0].selector.load_full();
2623        let builds = tracker.builds.load(Relaxed);
2624
2625        group.update().await.unwrap();
2626
2627        assert_eq!(group.backend_generation(), 1);
2628        assert_eq!(group.selector_generation(0), Some(1));
2629        assert_eq!(tracker.builds.load(Relaxed), builds);
2630        assert!(Arc::ptr_eq(&view_state, &group.backends.state.load_full()));
2631        assert!(Arc::ptr_eq(
2632            &registry_state,
2633            &group.backends.health_registry.state.load_full()
2634        ));
2635        assert!(Arc::ptr_eq(
2636            &selector,
2637            &group.selectors[0].selector.load_full()
2638        ));
2639    }
2640
2641    #[tokio::test]
2642    async fn test_load_balancer_group_shares_health_checks() {
2643        let b1 = Backend::new("1.0.0.1:80").unwrap();
2644        let b2 = Backend::new("1.1.1.1:80").unwrap();
2645        let discovery = Arc::new(MutableDiscovery::new(BTreeSet::from([
2646            b1.clone(),
2647            b2.clone(),
2648        ])));
2649        let checks = Arc::new(AtomicUsize::new(0));
2650
2651        let mut group = LoadBalancerGroup::<TestSelection>::from_backends_with_configs(
2652            Backends::new(Box::new(discovery)),
2653            [
2654                Some(TestSelectionConfig {
2655                    reverse: false,
2656                    tracker: None,
2657                }),
2658                Some(TestSelectionConfig {
2659                    reverse: false,
2660                    tracker: None,
2661                }),
2662            ],
2663        );
2664        group.set_health_check(Box::new(CountingHealthCheck {
2665            checks: checks.clone(),
2666        }));
2667
2668        group.update().await.unwrap();
2669        wait_for_group_generation(&group, 1).await;
2670        group.backends().run_health_check(false).await;
2671
2672        assert_eq!(checks.load(Relaxed), 2);
2673        group.backends().set_enable(&b1, false);
2674        assert_eq!(group.select(0, b"", 10), Some(b2.clone()));
2675        assert_eq!(group.select(1, b"", 10), Some(b2));
2676    }
2677
2678    #[test]
2679    #[should_panic(expected = "health check already configured")]
2680    fn test_health_registry_rejects_replacing_health_check() {
2681        let registry = HealthRegistry::new();
2682        let checks = Arc::new(AtomicUsize::new(0));
2683        registry.set_health_check(Box::new(CountingHealthCheck {
2684            checks: Arc::clone(&checks),
2685        }));
2686
2687        registry.set_health_check(Box::new(CountingHealthCheck { checks }));
2688    }
2689
2690    #[tokio::test]
2691    async fn test_view_drop_defers_removal_without_locking_views() {
2692        let backend = Backend::new("1.0.0.1:80").unwrap();
2693        let registry = Arc::new(HealthRegistry::new());
2694        let view = Backends::new_with_health_registry(
2695            Box::new(Arc::new(MutableDiscovery::new(BTreeSet::from([backend])))),
2696            Arc::clone(&registry),
2697        );
2698        view.update(|_| {}).await.unwrap();
2699        let view_id = view.view_id;
2700
2701        let views = registry
2702            .views
2703            .lock()
2704            .unwrap_or_else(|poisoned| poisoned.into_inner());
2705        drop(view);
2706        assert!(views.contains_key(&view_id));
2707        drop(views);
2708
2709        assert_eq!(registry.target_count(), 0);
2710        assert!(!registry
2711            .views
2712            .lock()
2713            .unwrap_or_else(|poisoned| poisoned.into_inner())
2714            .contains_key(&view_id));
2715    }
2716
2717    #[tokio::test]
2718    async fn test_health_registry_shares_probes_across_backend_views() {
2719        let shared = Backend::new("1.0.0.1:80").unwrap();
2720        let first_only = Backend::new("1.0.0.2:80").unwrap();
2721        let second_only = Backend::new("1.0.0.3:80").unwrap();
2722        let first_discovery = Arc::new(MutableDiscovery::new(BTreeSet::from([
2723            shared.clone(),
2724            first_only.clone(),
2725        ])));
2726        let second_discovery = Arc::new(MutableDiscovery::new(BTreeSet::from([
2727            shared.clone(),
2728            second_only.clone(),
2729        ])));
2730        let checks = Arc::new(AtomicUsize::new(0));
2731        let registry = Arc::new(HealthRegistry::new());
2732        registry.set_health_check(Box::new(SelectiveHealthCheck {
2733            checks: Arc::clone(&checks),
2734            unhealthy: Some(shared.addr.clone()),
2735        }));
2736
2737        let first = Backends::new_with_health_registry(
2738            Box::new(Arc::clone(&first_discovery)),
2739            Arc::clone(&registry),
2740        );
2741        let second = Backends::new_with_health_registry(
2742            Box::new(Arc::clone(&second_discovery)),
2743            Arc::clone(&registry),
2744        );
2745        first.update(|_| {}).await.unwrap();
2746        second.update(|_| {}).await.unwrap();
2747
2748        assert_eq!(registry.target_count(), 3);
2749        registry.run_health_check(false).await;
2750        assert_eq!(checks.load(Relaxed), 3);
2751        assert!(!first.ready(&shared));
2752        assert!(!second.ready(&shared));
2753        assert!(first.ready(&first_only));
2754        assert!(second.ready(&second_only));
2755        assert!(!first.ready(&second_only));
2756        assert!(!second.ready(&first_only));
2757
2758        first_discovery.replace(BTreeSet::new());
2759        first.update(|_| {}).await.unwrap();
2760        assert_eq!(registry.target_count(), 2);
2761        registry.run_health_check(false).await;
2762        assert_eq!(checks.load(Relaxed), 5);
2763
2764        second_discovery.replace(BTreeSet::new());
2765        second.update(|_| {}).await.unwrap();
2766        assert_eq!(registry.target_count(), 0);
2767        registry.run_health_check(false).await;
2768        assert_eq!(checks.load(Relaxed), 5);
2769    }
2770
2771    #[tokio::test]
2772    async fn test_health_registry_deduplicates_metadata_variants_by_address() {
2773        let first_backend = Backend::new_with_weight("1.0.0.1:80", 1).unwrap();
2774        let second_backend = Backend::new_with_weight("1.0.0.1:80", 2).unwrap();
2775        let checks = Arc::new(AtomicUsize::new(0));
2776        let registry = Arc::new(HealthRegistry::with_equivalence(address_health_key));
2777        registry.set_health_check(Box::new(CountingHealthCheck {
2778            checks: Arc::clone(&checks),
2779        }));
2780
2781        let first = Backends::new_with_health_registry(
2782            Box::new(Arc::new(MutableDiscovery::new(BTreeSet::from([
2783                first_backend.clone(),
2784            ])))),
2785            Arc::clone(&registry),
2786        );
2787        let second = Backends::new_with_health_registry(
2788            Box::new(Arc::new(MutableDiscovery::new(BTreeSet::from([
2789                second_backend.clone(),
2790            ])))),
2791            Arc::clone(&registry),
2792        );
2793        first.update(|_| {}).await.unwrap();
2794        second.update(|_| {}).await.unwrap();
2795
2796        assert_eq!(registry.target_count(), 1);
2797        registry.run_health_check(false).await;
2798        assert_eq!(checks.load(Relaxed), 1);
2799        assert!(first.ready(&first_backend));
2800        assert!(second.ready(&second_backend));
2801
2802        first.set_enable(&first_backend, false);
2803        assert!(!first.ready(&first_backend));
2804        assert!(second.ready(&second_backend));
2805    }
2806
2807    #[tokio::test]
2808    async fn test_different_health_registries_probe_independently() {
2809        let backend = Backend::new("1.0.0.1:80").unwrap();
2810        let discovery = Arc::new(MutableDiscovery::new(BTreeSet::from([backend])));
2811        let checks = Arc::new(AtomicUsize::new(0));
2812        let first_registry = Arc::new(HealthRegistry::new());
2813        let second_registry = Arc::new(HealthRegistry::new());
2814        first_registry.set_health_check(Box::new(CountingHealthCheck {
2815            checks: Arc::clone(&checks),
2816        }));
2817        second_registry.set_health_check(Box::new(CountingHealthCheck {
2818            checks: Arc::clone(&checks),
2819        }));
2820
2821        let first = Backends::new_with_health_registry(
2822            Box::new(Arc::clone(&discovery)),
2823            Arc::clone(&first_registry),
2824        );
2825        let second =
2826            Backends::new_with_health_registry(Box::new(discovery), Arc::clone(&second_registry));
2827        first.update(|_| {}).await.unwrap();
2828        second.update(|_| {}).await.unwrap();
2829
2830        first_registry.run_health_check(false).await;
2831        second_registry.run_health_check(false).await;
2832        assert_eq!(checks.load(Relaxed), 2);
2833    }
2834
2835    #[tokio::test]
2836    async fn test_backend_view_enablement_is_not_shared() {
2837        let backend = Backend::new("1.0.0.1:80").unwrap();
2838        let first_discovery = Arc::new(MutableDiscovery::new(BTreeSet::from([backend.clone()])));
2839        let second_discovery = Arc::new(MutableDiscovery::new(BTreeSet::from([backend.clone()])));
2840        let registry = Arc::new(HealthRegistry::new());
2841        let first = Backends::new_with_health_registry(
2842            Box::new(Arc::clone(&first_discovery)),
2843            Arc::clone(&registry),
2844        );
2845        let second =
2846            Backends::new_with_health_registry(Box::new(Arc::clone(&second_discovery)), registry);
2847        first.update(|_| {}).await.unwrap();
2848        second.update(|_| {}).await.unwrap();
2849        assert!(first.ready(&backend));
2850        assert!(second.ready(&backend));
2851
2852        first_discovery.set_enabled(&backend, false);
2853        first.update(|_| {}).await.unwrap();
2854        assert!(!first.ready(&backend));
2855        assert!(second.ready(&backend));
2856    }
2857
2858    #[tokio::test]
2859    async fn test_manual_enablement_survives_discovery_updates_without_override() {
2860        let first_backend = Backend::new("1.0.0.1:80").unwrap();
2861        let second_backend = Backend::new("1.0.0.2:80").unwrap();
2862        let discovery = Arc::new(MutableDiscovery::new(BTreeSet::from([
2863            first_backend.clone()
2864        ])));
2865        let backends = Backends::new(Box::new(Arc::clone(&discovery)));
2866
2867        backends.update(|_| {}).await.unwrap();
2868        backends.set_enable(&first_backend, false);
2869
2870        backends.update(|_| {}).await.unwrap();
2871        assert!(!backends.ready(&first_backend));
2872
2873        discovery.add(second_backend.clone());
2874        backends.update(|_| {}).await.unwrap();
2875        assert!(!backends.ready(&first_backend));
2876        assert!(backends.ready(&second_backend));
2877
2878        discovery.set_enabled(&first_backend, true);
2879        backends.update(|_| {}).await.unwrap();
2880        assert!(backends.ready(&first_backend));
2881    }
2882
2883    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2884    async fn test_discovery_override_updates_retained_snapshot_on_readd() {
2885        let old = Backend::new("1.0.0.1:80").unwrap();
2886        let replacement = Backend::new("1.0.0.2:80").unwrap();
2887        let discovery = Arc::new(MutableDiscovery::new(BTreeSet::from([old.clone()])));
2888        let tracker = Arc::new(BuildTracker::default());
2889        let group = LoadBalancerGroup::<TestSelection>::from_backends_with_configs(
2890            Backends::new(Box::new(Arc::clone(&discovery))),
2891            [Some(TestSelectionConfig {
2892                reverse: false,
2893                tracker: Some(Arc::clone(&tracker)),
2894            })],
2895        );
2896
2897        group.update().await.unwrap();
2898        wait_for_group_generation(&group, 1).await;
2899        group.backends().set_enable(&old, false);
2900        assert_eq!(group.select(0, b"", 10), None);
2901
2902        // Keep the old selector published while its backend leaves and then
2903        // returns in newer backend generations.
2904        tracker.reset();
2905        tracker.block();
2906        discovery.replace(BTreeSet::from([replacement]));
2907        group.update().await.unwrap();
2908        wait_for_active_builds(&tracker, 1).await;
2909        assert_eq!(group.selector_generation(0), Some(1));
2910        assert_eq!(group.select(0, b"", 10), None);
2911
2912        // An explicit discovery override is authoritative. Re-adding the exact
2913        // identity updates both current readiness and the retained selector's
2914        // shared enablement flag.
2915        discovery.set_enabled(&old, true);
2916        discovery.replace(BTreeSet::from([old.clone()]));
2917        group.update().await.unwrap();
2918        assert!(group.backends().ready(&old));
2919        assert_eq!(group.select(0, b"", 10), Some(old.clone()));
2920
2921        tracker.unblock();
2922        wait_for_group_generation(&group, 3).await;
2923        assert_eq!(group.select(0, b"", 10), Some(old));
2924    }
2925
2926    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2927    async fn test_manual_enablement_survives_transition_publication() {
2928        let first_backend = Backend::new("1.0.0.1:80").unwrap();
2929        let second_backend = Backend::new("1.0.0.2:80").unwrap();
2930        let discovery = Arc::new(MutableDiscovery::new(BTreeSet::from([
2931            first_backend.clone()
2932        ])));
2933        let backends = Arc::new(Backends::new(Box::new(Arc::clone(&discovery))));
2934        backends.update(|_| {}).await.unwrap();
2935
2936        let tracker = Arc::new(BuildTracker::default());
2937        tracker.block();
2938        discovery.add(second_backend);
2939        let update_task = tokio::spawn({
2940            let backends = Arc::clone(&backends);
2941            let tracker = Arc::clone(&tracker);
2942            async move {
2943                backends.update(|_| tracker.run_build()).await.unwrap();
2944            }
2945        });
2946
2947        wait_for_active_builds(&tracker, 1).await;
2948        backends.set_enable(&first_backend, false);
2949        tracker.unblock();
2950        update_task.await.unwrap();
2951
2952        assert!(!backends.ready(&first_backend));
2953    }
2954
2955    #[tokio::test]
2956    async fn test_health_check_service_runs_shared_registry_once() {
2957        let backend = Backend::new("1.0.0.1:80").unwrap();
2958        let discovery = Arc::new(MutableDiscovery::new(BTreeSet::from([backend])));
2959        let checks = Arc::new(AtomicUsize::new(0));
2960        let registry = Arc::new(HealthRegistry::new());
2961        registry.set_health_check(Box::new(CountingHealthCheck {
2962            checks: Arc::clone(&checks),
2963        }));
2964        let view = Backends::new_with_health_registry(Box::new(discovery), Arc::clone(&registry));
2965        view.update(|_| {}).await.unwrap();
2966
2967        let service = HealthCheckService::new(registry);
2968        let (_shutdown_tx, shutdown) = tokio::sync::watch::channel(false);
2969        service.run(shutdown, None).await;
2970
2971        assert_eq!(checks.load(Relaxed), 1);
2972    }
2973
2974    #[tokio::test]
2975    async fn test_health_check_service_without_check_does_not_signal_ready() {
2976        let service = Arc::new(HealthCheckService::new(Arc::new(HealthRegistry::new())));
2977        let (shutdown_tx, shutdown) = tokio::sync::watch::channel(false);
2978        let (ready_tx, ready_rx) = tokio::sync::watch::channel(false);
2979        let task = tokio::spawn({
2980            let service = Arc::clone(&service);
2981            async move {
2982                service
2983                    .run(shutdown, Some(ServiceReadyNotifier::new(ready_tx)))
2984                    .await;
2985            }
2986        });
2987
2988        tokio::task::yield_now().await;
2989        assert!(!*ready_rx.borrow());
2990        assert!(!task.is_finished());
2991
2992        shutdown_tx.send(true).unwrap();
2993        task.await.unwrap();
2994    }
2995
2996    #[tokio::test]
2997    async fn test_health_check_service_wakes_when_first_view_publishes_targets() {
2998        let backend = Backend::new("1.0.0.1:80").unwrap();
2999        let discovery = Arc::new(MutableDiscovery::new(BTreeSet::from([backend])));
3000        let checks = Arc::new(AtomicUsize::new(0));
3001        let registry = Arc::new(HealthRegistry::new());
3002        registry.set_health_check(Box::new(CountingHealthCheck {
3003            checks: Arc::clone(&checks),
3004        }));
3005
3006        let mut service = HealthCheckService::new(Arc::clone(&registry));
3007        service.health_check_frequency = Some(Duration::from_secs(3600));
3008        let service = Arc::new(service);
3009        let (shutdown_tx, shutdown) = tokio::sync::watch::channel(false);
3010        let service_task = tokio::spawn({
3011            let service = Arc::clone(&service);
3012            async move { service.run(shutdown, None).await }
3013        });
3014
3015        tokio::task::yield_now().await;
3016        let view = Backends::new_with_health_registry(Box::new(discovery), registry);
3017        view.update(|_| {}).await.unwrap();
3018
3019        tokio::time::timeout(Duration::from_secs(1), async {
3020            while checks.load(Relaxed) == 0 {
3021                tokio::task::yield_now().await;
3022            }
3023        })
3024        .await
3025        .expect("health service did not wake for newly published targets");
3026        assert_eq!(checks.load(Relaxed), 1);
3027
3028        shutdown_tx.send(true).unwrap();
3029        service_task.await.unwrap();
3030    }
3031
3032    #[tokio::test]
3033    async fn test_health_check_service_reconciles_dropped_view() {
3034        let backend = Backend::new("1.0.0.1:80").unwrap();
3035        let discovery = Arc::new(MutableDiscovery::new(BTreeSet::from([backend])));
3036        let checks = Arc::new(AtomicUsize::new(0));
3037        let registry = Arc::new(HealthRegistry::new());
3038        registry.set_health_check(Box::new(CountingHealthCheck {
3039            checks: Arc::clone(&checks),
3040        }));
3041        let view = Backends::new_with_health_registry(Box::new(discovery), Arc::clone(&registry));
3042        view.update(|_| {}).await.unwrap();
3043
3044        let mut service = HealthCheckService::new(Arc::clone(&registry));
3045        service.health_check_frequency = Some(Duration::from_secs(3600));
3046        let (shutdown_tx, shutdown) = tokio::sync::watch::channel(false);
3047        let service_task = tokio::spawn(async move { service.run(shutdown, None).await });
3048        tokio::time::timeout(Duration::from_secs(1), async {
3049            while checks.load(Relaxed) == 0 {
3050                tokio::task::yield_now().await;
3051            }
3052        })
3053        .await
3054        .expect("initial health check did not complete");
3055
3056        drop(view);
3057        tokio::time::timeout(Duration::from_secs(1), async {
3058            while !registry.state.load().targets.is_empty() {
3059                tokio::task::yield_now().await;
3060            }
3061        })
3062        .await
3063        .expect("health service did not reconcile the dropped view");
3064
3065        shutdown_tx.send(true).unwrap();
3066        service_task.await.unwrap();
3067    }
3068
3069    #[tokio::test]
3070    async fn test_registry_notifies_only_when_first_target_is_added() {
3071        let discovery = Arc::new(MutableDiscovery::new(BTreeSet::new()));
3072        let registry = Arc::new(HealthRegistry::new());
3073        let first = Backends::new_with_health_registry(
3074            Box::new(Arc::clone(&discovery)),
3075            Arc::clone(&registry),
3076        );
3077
3078        first.update(|_| {}).await.unwrap();
3079        assert!(tokio::time::timeout(
3080            Duration::from_millis(10),
3081            registry.targets_available.notified()
3082        )
3083        .await
3084        .is_err());
3085
3086        let backend = Backend::new("1.0.0.1:80").unwrap();
3087        discovery.add(backend.clone());
3088        first.update(|_| {}).await.unwrap();
3089        tokio::time::timeout(
3090            Duration::from_secs(1),
3091            registry.targets_available.notified(),
3092        )
3093        .await
3094        .expect("first target did not notify the registry");
3095
3096        let second = Backends::new_with_health_registry(
3097            Box::new(Arc::new(MutableDiscovery::new(BTreeSet::from([backend])))),
3098            Arc::clone(&registry),
3099        );
3100        second.update(|_| {}).await.unwrap();
3101        assert!(tokio::time::timeout(
3102            Duration::from_millis(10),
3103            registry.targets_available.notified()
3104        )
3105        .await
3106        .is_err());
3107    }
3108
3109    #[tokio::test]
3110    async fn test_health_check_service_shuts_down_while_waiting_for_targets() {
3111        let registry = Arc::new(HealthRegistry::new());
3112        registry.set_health_check(Box::new(CountingHealthCheck {
3113            checks: Arc::new(AtomicUsize::new(0)),
3114        }));
3115        let mut service = HealthCheckService::new(registry);
3116        service.health_check_frequency = Some(Duration::from_secs(3600));
3117
3118        let (shutdown_tx, shutdown) = tokio::sync::watch::channel(false);
3119        let service_task = tokio::spawn(async move { service.run(shutdown, None).await });
3120        tokio::task::yield_now().await;
3121        shutdown_tx.send(true).unwrap();
3122
3123        tokio::time::timeout(Duration::from_secs(1), service_task)
3124            .await
3125            .expect("health service did not stop while waiting for targets")
3126            .expect("health service task failed");
3127    }
3128
3129    // Keep a removed backend available until the selector rebuilds.
3130    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3131    async fn test_stale_group_selector_serves_removed_backend_until_converged() {
3132        let backend = Backend::new("1.0.0.1:80").unwrap();
3133        let discovery = Arc::new(MutableDiscovery::new(BTreeSet::from([backend.clone()])));
3134        let registry = Arc::new(HealthRegistry::new());
3135        let tracker = Arc::new(BuildTracker::default());
3136        let group = LoadBalancerGroup::<TestSelection>::from_backends_with_configs(
3137            Backends::new_with_health_registry(
3138                Box::new(Arc::clone(&discovery)),
3139                Arc::clone(&registry),
3140            ),
3141            [Some(TestSelectionConfig {
3142                reverse: false,
3143                tracker: Some(Arc::clone(&tracker)),
3144            })],
3145        );
3146        tracker.reset();
3147
3148        group.update().await.unwrap();
3149        wait_for_group_generation(&group, 1).await;
3150        assert_eq!(group.select(0, b"", 10), Some(backend.clone()));
3151
3152        // Remove the backend while the rebuild is blocked.
3153        tracker.block();
3154        discovery.replace(BTreeSet::new());
3155        group.update().await.unwrap();
3156        wait_for_active_builds(&tracker, 1).await;
3157
3158        assert_eq!(group.selector_generation(0), Some(1));
3159        assert_eq!(group.select(0, b"", 10), Some(backend.clone()));
3160        assert_eq!(registry.target_count(), 0);
3161
3162        // Finish the rebuild and remove the old readiness.
3163        tracker.unblock();
3164        wait_for_group_generation(&group, 2).await;
3165        assert_eq!(group.select(0, b"", 10), None);
3166        group.update().await.unwrap();
3167        assert!(!group.backends().ready(&backend));
3168    }
3169
3170    // Keep selection available during a disjoint membership change.
3171    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3172    async fn test_group_selection_available_across_disjoint_membership_swap() {
3173        let old = Backend::new("1.0.0.1:80").unwrap();
3174        let new = Backend::new("1.0.0.2:80").unwrap();
3175        let discovery = Arc::new(MutableDiscovery::new(BTreeSet::from([old.clone()])));
3176        let tracker = Arc::new(BuildTracker::default());
3177        let group = LoadBalancerGroup::<TestSelection>::from_backends_with_configs(
3178            Backends::new(Box::new(Arc::clone(&discovery))),
3179            [Some(TestSelectionConfig {
3180                reverse: false,
3181                tracker: Some(Arc::clone(&tracker)),
3182            })],
3183        );
3184        tracker.reset();
3185
3186        group.update().await.unwrap();
3187        wait_for_group_generation(&group, 1).await;
3188        assert_eq!(group.select(0, b"", 10), Some(old.clone()));
3189
3190        // Change membership while the rebuild is blocked.
3191        tracker.block();
3192        discovery.replace(BTreeSet::from([new.clone()]));
3193        group.update().await.unwrap();
3194        wait_for_active_builds(&tracker, 1).await;
3195        assert_eq!(group.selector_generation(0), Some(1));
3196        assert_eq!(group.backend_generation(), 2);
3197
3198        // The old selector can still use `old` through the readiness snapshot
3199        // it was published with, even though current membership dropped it.
3200        assert_eq!(group.select(0, b"", 10), Some(old.clone()));
3201        // Current-view readiness reports only the current membership.
3202        assert!(!group.backends().ready(&old));
3203        assert!(group.backends().ready(&new));
3204
3205        // The old selector's snapshot shares the removed backend's enablement
3206        // flag, reached through the interner, so manual disable/enable affects it.
3207        group.backends().set_enable(&old, false);
3208        assert_eq!(group.select(0, b"", 10), None);
3209        group.backends().set_enable(&old, true);
3210        assert_eq!(group.select(0, b"", 10), Some(old.clone()));
3211
3212        // Finish the rebuild. The old selector and its readiness snapshot are
3213        // replaced and released; no explicit prune is needed.
3214        tracker.unblock();
3215        wait_for_group_generation(&group, 2).await;
3216        assert_eq!(group.select(0, b"", 10), Some(new.clone()));
3217        assert!(!group.backends().ready(&old));
3218        assert!(group.backends().ready(&new));
3219        assert_eq!(group.select(0, b"", 10), Some(new));
3220    }
3221
3222    // Keep each backend identity paired with its own readiness while weights change.
3223    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3224    async fn test_group_weight_churn_preserves_selector_identity() {
3225        let addr = "1.0.0.1:80";
3226        let w100 = Backend::new_with_weight(addr, 100).unwrap();
3227        let w101 = Backend::new_with_weight(addr, 101).unwrap();
3228        let w102 = Backend::new_with_weight(addr, 102).unwrap();
3229        let discovery = Arc::new(MutableDiscovery::new(BTreeSet::from([w100.clone()])));
3230        let tracker = Arc::new(BuildTracker::default());
3231        let group = LoadBalancerGroup::<TestSelection>::from_backends_with_configs(
3232            Backends::new(Box::new(Arc::clone(&discovery))),
3233            [Some(TestSelectionConfig {
3234                reverse: false,
3235                tracker: Some(Arc::clone(&tracker)),
3236            })],
3237        );
3238
3239        group.update().await.unwrap();
3240        wait_for_group_generation(&group, 1).await;
3241        assert_eq!(group.select(0, b"", 10), Some(w100.clone()));
3242
3243        // Change the weight twice while the rebuild is blocked.
3244        tracker.reset();
3245        tracker.block();
3246        discovery.replace(BTreeSet::from([w101.clone()]));
3247        group.update().await.unwrap();
3248        discovery.replace(BTreeSet::from([w102.clone()]));
3249        group.update().await.unwrap();
3250        assert_eq!(group.backend_generation(), 3);
3251        wait_for_active_builds(&tracker, 1).await;
3252        assert_eq!(group.selector_generation(0), Some(1));
3253
3254        // Current-view readiness contains only the latest identity, while the
3255        // old selector retains the exact readiness paired with `w100`.
3256        assert_eq!(group.select(0, b"", 10), Some(w100.clone()));
3257        assert!(!group.backends().ready(&w100));
3258        assert!(!group.backends().ready(&w101));
3259        assert!(group.backends().ready(&w102));
3260
3261        // Disabling the current identity does not affect the old selector.
3262        group.backends().set_enable(&w102, false);
3263        assert!(!group.backends().ready(&w102));
3264        assert_eq!(group.select(0, b"", 10), Some(w100.clone()));
3265
3266        // The old identity can still be disabled through its retained handle.
3267        group.backends().set_enable(&w100, false);
3268        assert_eq!(group.select(0, b"", 10), None);
3269
3270        // Re-enable both independent identities before publication converges.
3271        group.backends().set_enable(&w100, true);
3272        assert_eq!(group.select(0, b"", 10), Some(w100.clone()));
3273        group.backends().set_enable(&w102, true);
3274        assert!(group.backends().ready(&w102));
3275
3276        // Finish the rebuild at the latest weight.
3277        tracker.unblock();
3278        wait_for_group_generation(&group, 3).await;
3279        assert_eq!(group.selector_generation(0), Some(3));
3280        assert_eq!(group.select(0, b"", 10), Some(w102.clone()));
3281
3282        // The latest identity is current and ready.
3283        assert!(group.backends().ready(&w102));
3284        assert_eq!(group.select(0, b"", 10), Some(w102));
3285    }
3286
3287    // An old selector keeps serving a backend whose address disappeared before
3288    // its rebuild, from the readiness snapshot it was published with.
3289    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3290    async fn test_group_old_selector_serves_disappeared_address_via_snapshot() {
3291        let old_addr = "1.0.0.1:80";
3292        let w100 = Backend::new_with_weight(old_addr, 100).unwrap();
3293        let w101 = Backend::new_with_weight(old_addr, 101).unwrap();
3294        let moved = Backend::new("1.0.0.2:80").unwrap();
3295        let discovery = Arc::new(MutableDiscovery::new(BTreeSet::from([w100.clone()])));
3296        let tracker = Arc::new(BuildTracker::default());
3297        let group = LoadBalancerGroup::<TestSelection>::from_backends_with_configs(
3298            Backends::new(Box::new(Arc::clone(&discovery))),
3299            [Some(TestSelectionConfig {
3300                reverse: false,
3301                tracker: Some(Arc::clone(&tracker)),
3302            })],
3303        );
3304
3305        group.update().await.unwrap();
3306        wait_for_group_generation(&group, 1).await;
3307        assert_eq!(group.select(0, b"", 10), Some(w100.clone()));
3308
3309        // Change the weight, then remove the address while blocked.
3310        tracker.reset();
3311        tracker.block();
3312        discovery.replace(BTreeSet::from([w101.clone()]));
3313        group.update().await.unwrap();
3314        discovery.replace(BTreeSet::from([moved.clone()]));
3315        group.update().await.unwrap();
3316        assert_eq!(group.backend_generation(), 3);
3317        wait_for_active_builds(&tracker, 1).await;
3318        assert_eq!(group.selector_generation(0), Some(1));
3319
3320        // The old selector keeps serving `w100` from its own readiness
3321        // snapshot, though its address left current membership.
3322        assert!(!group.backends().ready(&w100));
3323        assert_eq!(group.select(0, b"", 10), Some(w100.clone()));
3324        assert!(group.backends().ready(&moved));
3325
3326        // Finish the rebuild. The old selector and snapshot are released.
3327        tracker.unblock();
3328        wait_for_group_generation(&group, 3).await;
3329        assert_eq!(group.select(0, b"", 10), Some(moved.clone()));
3330        assert!(!group.backends().ready(&w100));
3331        assert!(group.backends().ready(&moved));
3332        assert_eq!(group.select(0, b"", 10), Some(moved));
3333    }
3334
3335    // An older published selector keeps selecting its own disjoint backend from
3336    // the readiness snapshot it was published with, even after a newer
3337    // generation is published. The snapshot's lifetime ends when the reader
3338    // drops it; no prune step runs.
3339    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3340    async fn test_old_published_snapshot_selects_disjoint_backend_after_update() {
3341        let old = Backend::new("1.0.0.1:80").unwrap();
3342        let new = Backend::new("1.0.0.2:80").unwrap();
3343        let discovery = Arc::new(MutableDiscovery::new(BTreeSet::from([old.clone()])));
3344        let group = LoadBalancerGroup::<TestSelection>::from_backends_with_configs(
3345            Backends::new(Box::new(Arc::clone(&discovery))),
3346            [Some(TestSelectionConfig {
3347                reverse: false,
3348                tracker: None,
3349            })],
3350        );
3351
3352        group.update().await.unwrap();
3353        wait_for_group_generation(&group, 1).await;
3354
3355        // Capture the generation-1 published selector and hold it across a later
3356        // update, as an in-flight request iterator would.
3357        let published = group.selectors[0].selector.load_full();
3358        assert!(published.readiness.ready(&old));
3359
3360        // Publish a disjoint generation 2.
3361        discovery.replace(BTreeSet::from([new.clone()]));
3362        group.update().await.unwrap();
3363        wait_for_group_generation(&group, 2).await;
3364
3365        // The held generation-1 snapshot still yields `old` and still marks it
3366        // ready, though current membership dropped it.
3367        let mut iter = published.selector.iter(b"");
3368        assert_eq!(iter.next(), Some(&old));
3369        assert!(published.readiness.ready(&old));
3370        assert!(!group.backends().ready(&old));
3371
3372        // Current selection uses the generation-2 snapshot.
3373        assert_eq!(group.select(0, b"", 10), Some(new.clone()));
3374        assert!(group.backends().ready(&new));
3375
3376        // Dropping the reader ends the old snapshot's lifetime without a prune.
3377        drop(iter);
3378        drop(published);
3379        assert_eq!(group.select(0, b"", 10), Some(new));
3380    }
3381
3382    // Two selectors published at different generations each consult the
3383    // readiness snapshot they were built with, not a shared current view.
3384    #[tokio::test]
3385    async fn test_selectors_at_different_generations_use_matching_snapshots() {
3386        let a = Backend::new("1.0.0.1:80").unwrap();
3387        let b = Backend::new("1.0.0.2:80").unwrap();
3388
3389        // Build two independent readiness snapshots from separate views.
3390        let backends_a =
3391            Backends::new(Box::new(Arc::new(MutableDiscovery::new(BTreeSet::from([
3392                a.clone(),
3393            ])))));
3394        backends_a.update(|_| {}).await.unwrap();
3395        let readiness_a = backends_a.readiness_snapshot();
3396        assert!(readiness_a.ready(&a));
3397
3398        let backends_b =
3399            Backends::new(Box::new(Arc::new(MutableDiscovery::new(BTreeSet::from([
3400                b.clone(),
3401            ])))));
3402        backends_b.update(|_| {}).await.unwrap();
3403        let readiness_b = backends_b.readiness_snapshot();
3404        assert!(readiness_b.ready(&b));
3405
3406        // A group with two selectors; publish each slot at a distinct generation
3407        // with its matching snapshot.
3408        let group = LoadBalancerGroup::<TestSelection>::from_backends_with_configs(
3409            Backends::new(Box::new(Arc::new(MutableDiscovery::new(BTreeSet::new())))),
3410            [
3411                Some(TestSelectionConfig {
3412                    reverse: false,
3413                    tracker: None,
3414                }),
3415                Some(TestSelectionConfig {
3416                    reverse: false,
3417                    tracker: None,
3418                }),
3419            ],
3420        );
3421        group.selectors[0]
3422            .selector
3423            .store(Arc::new(PublishedSelector::new(
3424                TestSelection::build(&BTreeSet::from([a.clone()])),
3425                readiness_a,
3426            )));
3427        group.selectors[0].generation.store(1, Release);
3428        group.selectors[1]
3429            .selector
3430            .store(Arc::new(PublishedSelector::new(
3431                TestSelection::build(&BTreeSet::from([b.clone()])),
3432                readiness_b,
3433            )));
3434        group.selectors[1].generation.store(2, Release);
3435
3436        // Each selector selects its own backend, marked ready by its own
3437        // snapshot.
3438        assert_eq!(group.select(0, b"", 10), Some(a.clone()));
3439        assert_eq!(group.select(1, b"", 10), Some(b.clone()));
3440
3441        // The snapshots are distinct: neither knows the other's backend.
3442        let published0 = group.selectors[0].selector.load();
3443        assert!(published0.readiness.ready(&a));
3444        assert!(!published0.readiness.ready(&b));
3445        let published1 = group.selectors[1].selector.load();
3446        assert!(published1.readiness.ready(&b));
3447        assert!(!published1.readiness.ready(&a));
3448    }
3449
3450    // The transition state published during a synchronous update callback must
3451    // cover both the outgoing and incoming readiness, so a request in the
3452    // callback (before the selector is swapped) does not lose the old backend.
3453    // Once the callback returns, only the current membership is ready.
3454    #[tokio::test]
3455    async fn test_transition_snapshot_covers_outgoing_and_incoming_readiness() {
3456        let a = Backend::new("1.0.0.1:80").unwrap();
3457        let b = Backend::new("1.0.0.2:80").unwrap();
3458        let discovery = Arc::new(MutableDiscovery::new(BTreeSet::from([a.clone()])));
3459        let backends = Backends::new(Box::new(Arc::clone(&discovery)));
3460
3461        backends.update(|_| {}).await.unwrap();
3462        assert!(backends.ready(&a));
3463
3464        // Disjoint swap A -> B. During the callback both must be ready.
3465        discovery.replace(BTreeSet::from([b.clone()]));
3466        let ran = AtomicBool::new(false);
3467        backends
3468            .update(|_| {
3469                ran.store(true, Relaxed);
3470                assert!(
3471                    backends.ready(&a),
3472                    "outgoing backend dropped mid-transition"
3473                );
3474                assert!(backends.ready(&b));
3475            })
3476            .await
3477            .unwrap();
3478        assert!(ran.load(Relaxed));
3479
3480        // After the callback the current view holds only B.
3481        assert!(!backends.ready(&a));
3482        assert!(backends.ready(&b));
3483    }
3484
3485    #[tokio::test]
3486    async fn test_current_readiness_uses_exact_backend_identity() {
3487        let old_backend = Backend::new_with_weight("1.0.0.1:80", 1).unwrap();
3488        let new_backend = Backend::new_with_weight("1.0.0.1:80", 2).unwrap();
3489        let discovery = Arc::new(MutableDiscovery::new(BTreeSet::from([old_backend.clone()])));
3490        let backends = Backends::new(Box::new(discovery.clone()));
3491
3492        backends.update(|_| {}).await.unwrap();
3493        assert!(backends.ready(&old_backend));
3494
3495        discovery.replace(BTreeSet::from([new_backend.clone()]));
3496        backends.update(|_| {}).await.unwrap();
3497        assert!(!backends.ready(&old_backend));
3498        assert!(backends.ready(&new_backend));
3499
3500        discovery.replace(BTreeSet::new());
3501        backends.update(|_| {}).await.unwrap();
3502        assert!(!backends.ready(&old_backend));
3503        assert!(!backends.ready(&new_backend));
3504    }
3505
3506    // Backend variants sharing an address retain independent enablement.
3507    #[tokio::test]
3508    async fn test_same_address_variants_have_independent_readiness() {
3509        let backend_v1 = Backend::new_with_weight("1.0.0.1:80", 1).unwrap();
3510        let backend_v2 = Backend::new_with_weight("1.0.0.1:80", 2).unwrap();
3511        let stale_backend = Backend::new_with_weight("1.0.0.1:80", 3).unwrap();
3512        let discovery = Arc::new(MutableDiscovery::new(BTreeSet::from([
3513            backend_v1.clone(),
3514            backend_v2.clone(),
3515        ])));
3516        let backends = Backends::new(Box::new(discovery));
3517
3518        backends.update(|_| {}).await.unwrap();
3519        assert!(backends.ready(&backend_v1));
3520        assert!(backends.ready(&backend_v2));
3521        assert!(!backends.ready(&stale_backend));
3522
3523        backends.set_enable(&backend_v1, false);
3524        assert!(!backends.ready(&backend_v1));
3525        assert!(backends.ready(&backend_v2));
3526        assert!(!backends.ready(&stale_backend));
3527    }
3528
3529    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
3530    async fn test_load_balancer_group_bounds_and_coalesces_rebuilds() {
3531        let b1 = Backend::new("1.0.0.1:80").unwrap();
3532        let b2 = Backend::new("1.1.1.1:80").unwrap();
3533        let b3 = Backend::new("1.1.1.2:80").unwrap();
3534        let b4 = Backend::new("1.1.1.3:80").unwrap();
3535        let b5 = Backend::new("1.1.1.4:80").unwrap();
3536        let discovery = Arc::new(MutableDiscovery::new(BTreeSet::from([b1.clone(), b2])));
3537        let tracker = Arc::new(BuildTracker::default());
3538        let configs = (0..6).map(|index| {
3539            Some(TestSelectionConfig {
3540                reverse: index % 2 == 1,
3541                tracker: Some(Arc::clone(&tracker)),
3542            })
3543        });
3544        let group = LoadBalancerGroup::<TestSelection>::from_backends_with_configs(
3545            Backends::new(Box::new(discovery.clone())),
3546            configs,
3547        );
3548        tracker.reset();
3549        tracker.block();
3550
3551        group.update().await.unwrap();
3552        assert_eq!(group.backend_generation(), 1);
3553        assert_eq!(group.selector_generation(0), Some(0));
3554        wait_for_active_builds(&tracker, 1).await;
3555
3556        discovery.add(b3);
3557        group.update().await.unwrap();
3558        discovery.add(b4);
3559        group.update().await.unwrap();
3560        discovery.add(b5.clone());
3561        group.update().await.unwrap();
3562        assert_eq!(group.backend_generation(), 4);
3563
3564        tracker.unblock();
3565        wait_for_group_generation(&group, 4).await;
3566
3567        assert_eq!(tracker.max_active.load(Relaxed), 1);
3568        assert_eq!(tracker.max_live_selectors.load(Relaxed), 7);
3569        assert_eq!(tracker.builds.load(Relaxed), 7);
3570        let mut coalesced_rebuilds = Vec::new();
3571        for selector_index in 0..group.selector_count() {
3572            assert_eq!(group.selector_generation(selector_index), Some(4));
3573            coalesced_rebuilds.push(group.selector_coalesced_rebuilds(selector_index).unwrap());
3574            assert_eq!(group.selector_failed_rebuilds(selector_index), Some(0));
3575            assert_eq!(
3576                group
3577                    .selector_last_update_timing(selector_index)
3578                    .map(|timing| timing.generation),
3579                Some(4)
3580            );
3581        }
3582        coalesced_rebuilds.sort_unstable();
3583        assert_eq!(coalesced_rebuilds, [2, 3, 3, 3, 3, 3]);
3584        assert_eq!(group.select(1, b"", 10), Some(b5));
3585        assert_eq!(group.select(0, b"", 10), Some(b1));
3586    }
3587
3588    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
3589    async fn test_shared_rebuild_gate_never_overlaps_retired_and_replacement_selectors() {
3590        let b1 = Backend::new("1.0.0.1:80").unwrap();
3591        let b2 = Backend::new("1.1.1.1:80").unwrap();
3592        let b3 = Backend::new("1.1.1.2:80").unwrap();
3593        let first_discovery = Arc::new(MutableDiscovery::new(BTreeSet::from([b1.clone()])));
3594        let second_discovery = Arc::new(MutableDiscovery::new(BTreeSet::from([b1.clone()])));
3595        let tracker = Arc::new(BuildTracker::default());
3596        let gate = Arc::new(SelectorRebuildGate::new());
3597        let config = || {
3598            [Some(TestSelectionConfig {
3599                reverse: false,
3600                tracker: Some(Arc::clone(&tracker)),
3601            })]
3602        };
3603
3604        let first = LoadBalancerGroup::<TestSelection>::from_backends_with_configs(
3605            Backends::new(Box::new(Arc::clone(&first_discovery))),
3606            config(),
3607        )
3608        .with_rebuild_gate(Arc::clone(&gate));
3609        let second = LoadBalancerGroup::<TestSelection>::from_backends_with_configs(
3610            Backends::new(Box::new(Arc::clone(&second_discovery))),
3611            config(),
3612        )
3613        .with_rebuild_gate(gate);
3614
3615        first.update().await.unwrap();
3616        second.update().await.unwrap();
3617        wait_for_group_generation(&first, 1).await;
3618        wait_for_group_generation(&second, 1).await;
3619        tracker.reset();
3620
3621        let retained_first_generation = first.selectors[0].selector.load_full();
3622        first_discovery.add(b2.clone());
3623        first.update().await.unwrap();
3624        wait_for_group_generation(&first, 2).await;
3625        wait_for_builds_started(&tracker, 1).await;
3626
3627        second_discovery.add(b2);
3628        second.update().await.unwrap();
3629        first_discovery.add(b3);
3630        first.update().await.unwrap();
3631        tokio::time::sleep(Duration::from_millis(25)).await;
3632
3633        assert_eq!(tracker.active.load(Relaxed), 0);
3634        assert_eq!(tracker.builds.load(Relaxed), 1);
3635        assert_eq!(tracker.live_selectors.load(Relaxed), 3);
3636        assert_eq!(tracker.max_live_selectors.load(Relaxed), 3);
3637
3638        drop(retained_first_generation);
3639        wait_for_group_generation(&first, 3).await;
3640        wait_for_group_generation(&second, 2).await;
3641
3642        assert_eq!(tracker.builds.load(Relaxed), 3);
3643        assert_eq!(tracker.max_active.load(Relaxed), 1);
3644        assert_eq!(tracker.max_live_selectors.load(Relaxed), 3);
3645    }
3646
3647    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3648    async fn test_rebuild_gate_cancellation_preserves_retired_selector() {
3649        let gate = Arc::new(SelectorRebuildGate::new());
3650        let permit = gate.acquire().await;
3651        let retired = Arc::new(PublishedSelector::new(
3652            TestSelection::build(&BTreeSet::new()),
3653            ReadinessSnapshot::empty(),
3654        ));
3655        let release_signal = retired.release_signal();
3656        gate.retire(&retired);
3657        drop(permit);
3658
3659        let waiting_gate = Arc::clone(&gate);
3660        let waiting = tokio::spawn(async move { waiting_gate.acquire().await });
3661        tokio::time::timeout(Duration::from_secs(5), async {
3662            while Arc::strong_count(&release_signal) < 4 {
3663                tokio::task::yield_now().await;
3664            }
3665        })
3666        .await
3667        .expect("gate waiter did not start waiting for the retired selector");
3668
3669        waiting.abort();
3670        assert!(waiting.await.unwrap_err().is_cancelled());
3671        assert!(gate
3672            .retired
3673            .lock()
3674            .unwrap_or_else(|poisoned| poisoned.into_inner())
3675            .as_ref()
3676            .is_some_and(|current| Arc::ptr_eq(current, &release_signal)));
3677
3678        let next_gate = Arc::clone(&gate);
3679        let next = tokio::spawn(async move { next_gate.acquire().await });
3680        tokio::time::timeout(Duration::from_secs(5), async {
3681            while Arc::strong_count(&release_signal) < 4 {
3682                tokio::task::yield_now().await;
3683            }
3684        })
3685        .await
3686        .expect("next gate acquire did not wait for the retired selector");
3687        assert!(!next.is_finished());
3688
3689        drop(retired);
3690        let _ = tokio::time::timeout(Duration::from_secs(5), next)
3691            .await
3692            .expect("next gate acquire did not observe selector release")
3693            .expect("next gate acquire task failed");
3694    }
3695
3696    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
3697    async fn test_cancelled_selector_destruction_holds_rebuild_gate() {
3698        let first_backend = Backend::new("1.0.0.1:80").unwrap();
3699        let second_backend = Backend::new("1.0.0.2:80").unwrap();
3700        let first_discovery = Arc::new(MutableDiscovery::new(BTreeSet::from([first_backend])));
3701        let second_discovery = Arc::new(MutableDiscovery::new(BTreeSet::from([second_backend])));
3702        let tracker = Arc::new(BuildTracker::default());
3703        let gate = Arc::new(SelectorRebuildGate::new());
3704        let config = || {
3705            [Some(TestSelectionConfig {
3706                reverse: false,
3707                tracker: Some(Arc::clone(&tracker)),
3708            })]
3709        };
3710        let first = LoadBalancerGroup::<TestSelection>::from_backends_with_configs(
3711            Backends::new(Box::new(first_discovery)),
3712            config(),
3713        )
3714        .with_rebuild_gate(Arc::clone(&gate));
3715        let second = LoadBalancerGroup::<TestSelection>::from_backends_with_configs(
3716            Backends::new(Box::new(second_discovery)),
3717            config(),
3718        )
3719        .with_rebuild_gate(gate);
3720        tracker.reset();
3721        tracker.block();
3722
3723        first.update().await.unwrap();
3724        wait_for_active_builds(&tracker, 1).await;
3725        let first_slot = Arc::clone(&first.selectors[0]);
3726        drop(first);
3727        second.update().await.unwrap();
3728
3729        tracker.block_drop();
3730        tracker.unblock();
3731        tokio::time::timeout(Duration::from_secs(5), async {
3732            while tracker.drop_waiters.load(Relaxed) == 0 {
3733                tokio::task::yield_now().await;
3734            }
3735        })
3736        .await
3737        .expect("cancelled selector destruction did not start");
3738
3739        assert_eq!(tracker.builds.load(Relaxed), 1);
3740        assert_eq!(second.selector_generation(0), Some(0));
3741
3742        tracker.unblock_drop();
3743        wait_for_group_generation(&second, 1).await;
3744        drop(first_slot);
3745        assert_eq!(tracker.builds.load(Relaxed), 2);
3746    }
3747
3748    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3749    async fn test_selector_rebuild_task_abort_restores_in_flight_request() {
3750        let backend = Backend::new("1.0.0.1:80").unwrap();
3751        let tracker = Arc::new(BuildTracker::default());
3752        let group = LoadBalancerGroup::<TestSelection>::from_backends_with_configs(
3753            Backends::new(Box::new(Arc::new(MutableDiscovery::new(BTreeSet::new())))),
3754            [Some(TestSelectionConfig {
3755                reverse: false,
3756                tracker: Some(Arc::clone(&tracker)),
3757            })],
3758        );
3759        tracker.reset();
3760        tracker.block();
3761
3762        // A distinguishable readiness snapshot travels with the request, so the
3763        // abort path must restore it alongside membership and generation.
3764        let readiness = ReadinessSnapshot(Arc::new(HashMap::from([(
3765            backend.hash_key(),
3766            BackendReadiness {
3767                enabled: Arc::new(AtomicBool::new(true)),
3768                health: Health::default(),
3769            },
3770        )])));
3771        let slot = Arc::clone(&group.selectors[0]);
3772        {
3773            let mut rebuild_state = lock_rebuild_state(&slot.rebuild_state);
3774            rebuild_state.is_running = true;
3775            rebuild_state.pending_request = Some(SelectorRebuildRequest {
3776                generation: 1,
3777                backends: Arc::new(BTreeSet::from([backend.clone()])),
3778                readiness,
3779                requested_at: Instant::now(),
3780            });
3781        }
3782        let rebuild_task = tokio::spawn(run_selector_rebuilds(
3783            Arc::clone(&slot),
3784            Arc::clone(&group.rebuild_gate),
3785            Arc::clone(&group.rebuild_notify),
3786            Arc::clone(&group.rebuild_cancellation),
3787        ));
3788        wait_for_active_builds(&tracker, 1).await;
3789
3790        rebuild_task.abort();
3791        assert!(rebuild_task.await.unwrap_err().is_cancelled());
3792        {
3793            let state = lock_rebuild_state(&slot.rebuild_state);
3794            assert!(!state.is_running);
3795            assert_eq!(
3796                state
3797                    .pending_request
3798                    .as_ref()
3799                    .map(|request| request.generation),
3800                Some(1)
3801            );
3802            assert!(
3803                state
3804                    .pending_request
3805                    .as_ref()
3806                    .is_some_and(|request| request.readiness.ready(&backend)),
3807                "aborted rebuild lost its readiness bundle"
3808            );
3809        }
3810
3811        tracker.unblock();
3812        tokio::time::timeout(Duration::from_secs(5), async {
3813            while tracker.active.load(Relaxed) > 0 {
3814                tokio::task::yield_now().await;
3815            }
3816        })
3817        .await
3818        .expect("detached blocking build did not finish");
3819    }
3820
3821    #[tokio::test]
3822    async fn test_selector_rebuild_task_guard_cleans_up_after_panic() {
3823        let group = LoadBalancerGroup::<TestSelection>::from_backends_with_configs(
3824            Backends::new(Box::new(Arc::new(MutableDiscovery::new(BTreeSet::new())))),
3825            [Some(TestSelectionConfig {
3826                reverse: false,
3827                tracker: None,
3828            })],
3829        );
3830        let slot = Arc::clone(&group.selectors[0]);
3831        lock_rebuild_state(&slot.rebuild_state).is_running = true;
3832        let rebuild_notify = Arc::new(Notify::new());
3833
3834        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe({
3835            let slot = Arc::clone(&slot);
3836            let rebuild_notify = Arc::clone(&rebuild_notify);
3837            move || {
3838                let _guard = SelectorRebuildTaskGuard::new(slot, rebuild_notify);
3839                panic!("intentional selector rebuild panic");
3840            }
3841        }));
3842
3843        assert!(result.is_err());
3844        assert!(!lock_rebuild_state(&slot.rebuild_state).is_running);
3845        assert_eq!(slot.failed_rebuilds.load(Relaxed), 1);
3846        tokio::time::timeout(Duration::from_secs(1), rebuild_notify.notified())
3847            .await
3848            .expect("panic cleanup did not notify the background loop");
3849    }
3850
3851    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3852    async fn test_load_balancer_group_waits_for_initial_selectors_before_ready() {
3853        let backend = Backend::new("1.0.0.1:80").unwrap();
3854        let discovery = Arc::new(MutableDiscovery::new(BTreeSet::from([backend])));
3855        let tracker = Arc::new(BuildTracker::default());
3856        let group = Arc::new(
3857            LoadBalancerGroup::<TestSelection>::from_backends_with_configs(
3858                Backends::new(Box::new(discovery)),
3859                [Some(TestSelectionConfig {
3860                    reverse: false,
3861                    tracker: Some(Arc::clone(&tracker)),
3862                })],
3863            ),
3864        );
3865        tracker.reset();
3866        tracker.block();
3867
3868        let (_shutdown_tx, shutdown) = tokio::sync::watch::channel(false);
3869        let (ready_tx, mut ready_rx) = tokio::sync::watch::channel(false);
3870        let run_group = Arc::clone(&group);
3871        let run_task = tokio::spawn(async move {
3872            run_group
3873                .run(shutdown, Some(ServiceReadyNotifier::new(ready_tx)))
3874                .await;
3875        });
3876
3877        wait_for_active_builds(&tracker, 1).await;
3878        assert!(!*ready_rx.borrow());
3879
3880        tracker.unblock();
3881        tokio::time::timeout(Duration::from_secs(5), ready_rx.changed())
3882            .await
3883            .expect("ready notification timed out")
3884            .expect("ready notifier was dropped");
3885        assert!(*ready_rx.borrow());
3886        run_task.await.expect("group background task failed");
3887    }
3888
3889    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3890    async fn test_load_balancer_group_waits_for_successful_update_before_ready() {
3891        let discovery = Arc::new(FailThenWaitDiscovery {
3892            backend: Backend::new("1.0.0.1:80").unwrap(),
3893            attempts: AtomicUsize::new(0),
3894            allow_success: Notify::new(),
3895        });
3896        let mut group = LoadBalancerGroup::<TestSelection>::from_backends_with_configs(
3897            Backends::new(Box::new(Arc::clone(&discovery))),
3898            [Some(TestSelectionConfig {
3899                reverse: false,
3900                tracker: None,
3901            })],
3902        );
3903        group.update_frequency = Some(Duration::from_millis(1));
3904        let group = Arc::new(group);
3905
3906        let (shutdown_tx, shutdown) = tokio::sync::watch::channel(false);
3907        let (ready_tx, mut ready_rx) = tokio::sync::watch::channel(false);
3908        let run_group = Arc::clone(&group);
3909        let run_task = tokio::spawn(async move {
3910            run_group
3911                .run(shutdown, Some(ServiceReadyNotifier::new(ready_tx)))
3912                .await;
3913        });
3914
3915        tokio::time::timeout(Duration::from_secs(5), async {
3916            while discovery.attempts.load(Relaxed) < 2 {
3917                tokio::time::sleep(Duration::from_millis(1)).await;
3918            }
3919        })
3920        .await
3921        .expect("second discovery attempt did not start");
3922        assert!(!*ready_rx.borrow());
3923
3924        discovery.allow_success.notify_one();
3925        tokio::time::timeout(Duration::from_secs(5), ready_rx.changed())
3926            .await
3927            .expect("ready notification timed out")
3928            .expect("ready notifier was dropped");
3929        assert!(*ready_rx.borrow());
3930
3931        shutdown_tx.send(true).unwrap();
3932        tokio::time::timeout(Duration::from_secs(5), run_task)
3933            .await
3934            .expect("load balancer group did not stop")
3935            .expect("load balancer group task failed");
3936    }
3937
3938    // Finding 1: a blocking selector build must not open a fail-closed window;
3939    // the previously published selection stays usable until the new selector is
3940    // built and published, then the new selection becomes usable.
3941    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3942    async fn test_select_during_blocking_update_keeps_old_backend_ready() {
3943        let old = Backend::new("1.0.0.1:80").unwrap();
3944        let new = Backend::new("1.0.0.2:80").unwrap();
3945        let discovery = Arc::new(MutableDiscovery::new(BTreeSet::from([old.clone()])));
3946        let tracker = Arc::new(BuildTracker::default());
3947        let lb = Arc::new(LoadBalancer::<TestSelection>::from_backends_with_config(
3948            Backends::new(Box::new(Arc::clone(&discovery))),
3949            Some(TestSelectionConfig {
3950                reverse: false,
3951                tracker: Some(Arc::clone(&tracker)),
3952            }),
3953        ));
3954
3955        lb.update().await.unwrap();
3956        assert_eq!(lb.select(b"", 10), Some(old.clone()));
3957
3958        tracker.reset();
3959        tracker.block();
3960        discovery.replace(BTreeSet::from([new.clone()]));
3961        let update_lb = Arc::clone(&lb);
3962        let update_task = tokio::spawn(async move { update_lb.update().await.unwrap() });
3963
3964        // The replacement selector is still being built; the old selection and
3965        // its readiness must remain usable for the whole build.
3966        wait_for_active_builds(&tracker, 1).await;
3967        assert_eq!(lb.select(b"", 10), Some(old.clone()));
3968        assert!(lb.backends().ready(&old));
3969
3970        tracker.unblock();
3971        update_task.await.unwrap();
3972
3973        assert_eq!(lb.select(b"", 10), Some(new.clone()));
3974        assert!(lb.backends().ready(&new));
3975        assert!(!lb.backends().ready(&old));
3976    }
3977
3978    #[tokio::test]
3979    async fn test_new_selector_is_ready_before_update_callback_returns() {
3980        let old = Backend::new("1.0.0.1:80").unwrap();
3981        let new = Backend::new("1.0.0.2:80").unwrap();
3982        let discovery = Arc::new(MutableDiscovery::new(BTreeSet::from([old.clone()])));
3983        let backends = Backends::new(Box::new(Arc::clone(&discovery)));
3984        backends.update(|_| {}).await.unwrap();
3985        let selector = ArcSwap::new(Arc::new(TestSelection::build(&BTreeSet::from([
3986            old.clone()
3987        ]))));
3988
3989        discovery.replace(BTreeSet::from([new.clone()]));
3990        backends
3991            .update(|members| {
3992                selector.store(Arc::new(TestSelection::build(&members)));
3993                let selected = selector.load().backends[0].clone();
3994                assert_eq!(selected, new);
3995                assert!(backends.ready(&selected));
3996                assert!(backends.ready(&old));
3997            })
3998            .await
3999            .unwrap();
4000
4001        assert!(backends.ready(&new));
4002        assert!(!backends.ready(&old));
4003    }
4004
4005    // Finding 2: one-shot mode must wait for the first target before running its
4006    // single pass and signaling ready, even if the service starts before any
4007    // target is published.
4008    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
4009    async fn test_health_check_service_one_shot_waits_for_first_target() {
4010        let backend = Backend::new("1.0.0.1:80").unwrap();
4011        let discovery = Arc::new(MutableDiscovery::new(BTreeSet::new()));
4012        let checks = Arc::new(AtomicUsize::new(0));
4013        let registry = Arc::new(HealthRegistry::new());
4014        registry.set_health_check(Box::new(CountingHealthCheck {
4015            checks: Arc::clone(&checks),
4016        }));
4017        let view = Backends::new_with_health_registry(
4018            Box::new(Arc::clone(&discovery)),
4019            Arc::clone(&registry),
4020        );
4021        view.update(|_| {}).await.unwrap();
4022
4023        // One-shot: health_check_frequency defaults to None.
4024        let service = HealthCheckService::new(Arc::clone(&registry));
4025        let (shutdown_tx, shutdown) = tokio::sync::watch::channel(false);
4026        let (ready_tx, ready_rx) = tokio::sync::watch::channel(false);
4027        let task = tokio::spawn(async move {
4028            service
4029                .run(shutdown, Some(ServiceReadyNotifier::new(ready_tx)))
4030                .await;
4031        });
4032
4033        // With an empty registry the single pass must not run or signal ready.
4034        tokio::time::sleep(Duration::from_millis(25)).await;
4035        assert_eq!(checks.load(Relaxed), 0);
4036        assert!(!*ready_rx.borrow());
4037        assert!(!task.is_finished());
4038
4039        // Publishing the first target lets the single pass run and complete.
4040        discovery.add(backend);
4041        view.update(|_| {}).await.unwrap();
4042
4043        tokio::time::timeout(Duration::from_secs(5), task)
4044            .await
4045            .expect("one-shot health service did not finish")
4046            .expect("health service task failed");
4047        assert_eq!(checks.load(Relaxed), 1);
4048        assert!(*ready_rx.borrow());
4049
4050        drop(shutdown_tx);
4051    }
4052
4053    // Finding 3: readiness caches the shared health handle in the view state, so
4054    // health observations (and reconciliation triggered by other views) stay
4055    // visible without a second registry snapshot.
4056    #[tokio::test]
4057    async fn test_shared_health_update_visible_after_other_view_reconcile() {
4058        let backend = Backend::new("1.0.0.1:80").unwrap();
4059        let other = Backend::new("1.0.0.2:80").unwrap();
4060        let registry = Arc::new(HealthRegistry::new());
4061        registry.set_health_check(Box::new(SelectiveHealthCheck {
4062            checks: Arc::new(AtomicUsize::new(0)),
4063            unhealthy: Some(backend.addr.clone()),
4064        }));
4065        let first_discovery = Arc::new(MutableDiscovery::new(BTreeSet::from([backend.clone()])));
4066        let first = Backends::new_with_health_registry(
4067            Box::new(Arc::clone(&first_discovery)),
4068            Arc::clone(&registry),
4069        );
4070        first.update(|_| {}).await.unwrap();
4071        assert!(first.ready(&backend));
4072
4073        // Flip the shared health to unhealthy.
4074        registry.run_health_check(false).await;
4075        assert!(!first.ready(&backend));
4076
4077        // A second view joining triggers reconciliation; the cached handle must
4078        // still reflect the shared unhealthy state (reconcile preserves it).
4079        let second = Backends::new_with_health_registry(
4080            Box::new(Arc::new(MutableDiscovery::new(BTreeSet::from([
4081                other.clone()
4082            ])))),
4083            Arc::clone(&registry),
4084        );
4085        second.update(|_| {}).await.unwrap();
4086        assert!(!first.ready(&backend));
4087
4088        // A membership change on the first view re-snapshots handles and must
4089        // continue to observe the shared unhealthy state.
4090        first_discovery.add(other.clone());
4091        first.update(|_| {}).await.unwrap();
4092        assert!(!first.ready(&backend));
4093        assert!(first.ready(&other));
4094    }
4095
4096    // Finding 4: a selector whose build fails once must still converge to the
4097    // desired generation without another discovery change.
4098    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
4099    async fn test_group_selector_retries_after_failed_build() {
4100        let backend = Backend::new("1.0.0.1:80").unwrap();
4101        let discovery = Arc::new(MutableDiscovery::new(BTreeSet::from([backend.clone()])));
4102        let tracker = Arc::new(BuildTracker::default());
4103        let group = LoadBalancerGroup::<TestSelection>::from_backends_with_configs(
4104            Backends::new(Box::new(Arc::clone(&discovery))),
4105            [Some(TestSelectionConfig {
4106                reverse: false,
4107                tracker: Some(Arc::clone(&tracker)),
4108            })],
4109        );
4110
4111        // The construction build already ran; make the next build panic once.
4112        tracker.set_panics(1);
4113        group.update().await.unwrap();
4114        assert_eq!(group.backend_generation(), 1);
4115
4116        // Despite the first rebuild panicking, the selector converges via retry.
4117        wait_for_group_generation(&group, 1).await;
4118        assert_eq!(group.selector_failed_rebuilds(0), Some(1));
4119        assert_eq!(group.select(0, b"", 10), Some(backend));
4120    }
4121
4122    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
4123    async fn test_new_generation_interrupts_rebuild_backoff() {
4124        let first = Backend::new("1.0.0.1:80").unwrap();
4125        let second = Backend::new("1.0.0.2:80").unwrap();
4126        let discovery = Arc::new(MutableDiscovery::new(BTreeSet::from([first])));
4127        let tracker = Arc::new(BuildTracker::default());
4128        let group = LoadBalancerGroup::<TestSelection>::from_backends_with_configs(
4129            Backends::new(Box::new(Arc::clone(&discovery))),
4130            [Some(TestSelectionConfig {
4131                reverse: false,
4132                tracker: Some(Arc::clone(&tracker)),
4133            })],
4134        );
4135
4136        tracker.set_panics(20);
4137        group.update().await.unwrap();
4138        tokio::time::timeout(Duration::from_secs(5), async {
4139            while group.selector_failed_rebuilds(0).unwrap_or_default() < 6 {
4140                tokio::time::sleep(Duration::from_millis(1)).await;
4141            }
4142        })
4143        .await
4144        .expect("selector did not enter exponential backoff");
4145
4146        tracker.set_panics(0);
4147        discovery.add(second);
4148        group.update().await.unwrap();
4149        tokio::time::timeout(Duration::from_millis(150), async {
4150            while group.selector_generation(0) != Some(2) {
4151                tokio::time::sleep(Duration::from_millis(1)).await;
4152            }
4153        })
4154        .await
4155        .expect("new generation did not interrupt stale rebuild backoff");
4156    }
4157
4158    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
4159    async fn test_group_drop_cancels_failed_rebuild_retries() {
4160        let backend = Backend::new("1.0.0.1:80").unwrap();
4161        let discovery = Arc::new(MutableDiscovery::new(BTreeSet::from([backend])));
4162        let tracker = Arc::new(BuildTracker::default());
4163        let group = LoadBalancerGroup::<TestSelection>::from_backends_with_configs(
4164            Backends::new(Box::new(discovery)),
4165            [Some(TestSelectionConfig {
4166                reverse: false,
4167                tracker: Some(Arc::clone(&tracker)),
4168            })],
4169        );
4170
4171        tracker.set_panics(usize::MAX);
4172        group.update().await.unwrap();
4173        tokio::time::timeout(Duration::from_secs(5), async {
4174            while group.selector_failed_rebuilds(0).unwrap_or_default() < 2 {
4175                tokio::time::sleep(Duration::from_millis(1)).await;
4176            }
4177        })
4178        .await
4179        .expect("selector did not begin retrying failed rebuilds");
4180
4181        let slot = Arc::clone(&group.selectors[0]);
4182        drop(group);
4183        tokio::time::timeout(Duration::from_secs(5), async {
4184            while lock_rebuild_state(&slot.rebuild_state).is_running {
4185                tokio::time::sleep(Duration::from_millis(1)).await;
4186            }
4187        })
4188        .await
4189        .expect("selector rebuild task did not stop after group drop");
4190        let remaining_panics = tracker.panics.load(Relaxed);
4191        tokio::time::sleep(Duration::from_millis(100)).await;
4192        assert_eq!(tracker.panics.load(Relaxed), remaining_panics);
4193    }
4194
4195    // Finding 5: before readiness is signaled, every successful update advances
4196    // the readiness target to the latest generation, so a burst of updates does
4197    // not let a stale selector satisfy readiness.
4198    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
4199    async fn test_group_readiness_tracks_latest_generation_during_burst() {
4200        let b1 = Backend::new("1.0.0.1:80").unwrap();
4201        let b2 = Backend::new("1.1.1.1:80").unwrap();
4202        let discovery = Arc::new(MutableDiscovery::new(BTreeSet::from([b1.clone()])));
4203        let tracker = Arc::new(BuildTracker::default());
4204        let mut group = LoadBalancerGroup::<TestSelection>::from_backends_with_configs(
4205            Backends::new(Box::new(Arc::clone(&discovery))),
4206            [Some(TestSelectionConfig {
4207                reverse: false,
4208                tracker: Some(Arc::clone(&tracker)),
4209            })],
4210        );
4211        group.update_frequency = Some(Duration::from_millis(5));
4212        let group = Arc::new(group);
4213        tracker.reset();
4214        // Hold the generation-1 build so generation 2 can arrive first.
4215        tracker.block();
4216
4217        let (shutdown_tx, shutdown) = tokio::sync::watch::channel(false);
4218        let (ready_tx, mut ready_rx) = tokio::sync::watch::channel(false);
4219        let run_group = Arc::clone(&group);
4220        let run_task = tokio::spawn(async move {
4221            run_group
4222                .run(shutdown, Some(ServiceReadyNotifier::new(ready_tx)))
4223                .await;
4224        });
4225
4226        wait_for_active_builds(&tracker, 1).await;
4227        assert_eq!(group.backend_generation(), 1);
4228
4229        // Generation 2 arrives while the generation-1 build is still blocked.
4230        discovery.add(b2.clone());
4231        tokio::time::timeout(Duration::from_secs(5), async {
4232            while group.backend_generation() < 2 {
4233                tokio::time::sleep(Duration::from_millis(1)).await;
4234            }
4235        })
4236        .await
4237        .expect("generation 2 did not register");
4238
4239        // Let generation 1 complete but deterministically block generation 2's
4240        // build (the second build, index 1).
4241        tracker.set_block_from(1);
4242        tracker.unblock();
4243
4244        tokio::time::timeout(Duration::from_secs(5), async {
4245            while group.selector_generation(0) != Some(1) {
4246                tokio::time::sleep(Duration::from_millis(1)).await;
4247            }
4248        })
4249        .await
4250        .expect("generation 1 selector did not publish");
4251        wait_for_active_builds(&tracker, 1).await;
4252
4253        // Current membership is generation 2 but the selector is at generation
4254        // 1, so readiness must not fire yet.
4255        tokio::time::sleep(Duration::from_millis(30)).await;
4256        assert!(
4257            !*ready_rx.borrow(),
4258            "readiness fired while a selector was still stale"
4259        );
4260
4261        // Releasing generation 2 lets the selector converge and readiness fire.
4262        tracker.set_block_from(usize::MAX);
4263        tracker.unblock();
4264        tokio::time::timeout(Duration::from_secs(5), ready_rx.changed())
4265            .await
4266            .expect("ready notification timed out")
4267            .expect("ready notifier was dropped");
4268        assert!(*ready_rx.borrow());
4269        assert_eq!(group.selector_generation(0), Some(2));
4270
4271        shutdown_tx.send(true).unwrap();
4272        let _ = tokio::time::timeout(Duration::from_secs(5), run_task).await;
4273    }
4274
4275    // Finding 6: the release signal for a retired selector must not fire until
4276    // the selector (including references held by in-flight iterators) is fully
4277    // destroyed, so the next build cannot start early.
4278    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
4279    async fn test_selector_release_waits_for_iterator_and_destructor() {
4280        let b1 = Backend::new("1.0.0.1:80").unwrap();
4281        let b2 = Backend::new("1.1.1.1:80").unwrap();
4282        let b3 = Backend::new("1.1.1.2:80").unwrap();
4283        let discovery = Arc::new(MutableDiscovery::new(BTreeSet::from([b1.clone()])));
4284        let tracker = Arc::new(BuildTracker::default());
4285        let group = Arc::new(
4286            LoadBalancerGroup::<TestSelection>::from_backends_with_configs(
4287                Backends::new(Box::new(Arc::clone(&discovery))),
4288                [Some(TestSelectionConfig {
4289                    reverse: false,
4290                    tracker: Some(Arc::clone(&tracker)),
4291                })],
4292            ),
4293        );
4294
4295        group.update().await.unwrap();
4296        wait_for_group_generation(&group, 1).await;
4297        tracker.reset();
4298
4299        // Hold a live iterator over the generation-1 selector. It keeps both the
4300        // published selector guard and a separate `Arc<S>` clone alive. The
4301        // tuple drops `iter` (and its `Arc<S>` clone) before the guard, matching
4302        // the release ordering `select_with` relies on.
4303        let published = group.selectors[0].selector.load();
4304        let iter = published.selector.iter(b"");
4305        let reader = (iter, published);
4306
4307        // Make the eventual destructor of the generation-1 selector block.
4308        tracker.block_drop();
4309
4310        // Generation 2 builds a replacement and retires generation 1.
4311        discovery.add(b2);
4312        group.update().await.unwrap();
4313        wait_for_builds_started(&tracker, 1).await;
4314
4315        // Generation 3 is queued but cannot build until generation 1 is released.
4316        discovery.add(b3);
4317        group.update().await.unwrap();
4318
4319        // While the reader holds the retired selector, generation 3 must not
4320        // build.
4321        tokio::time::sleep(Duration::from_millis(25)).await;
4322        assert_eq!(tracker.builds.load(Relaxed), 1);
4323
4324        // Dropping the reader begins destruction of generation 1 on a blocking
4325        // thread; the destructor is held, so release still must not fire and
4326        // generation 3 still must not build.
4327        let drop_reader = tokio::task::spawn_blocking(move || drop(reader));
4328        tokio::time::sleep(Duration::from_millis(25)).await;
4329        assert_eq!(
4330            tracker.builds.load(Relaxed),
4331            1,
4332            "next build started before the retired selector was destroyed"
4333        );
4334
4335        // Completing destruction releases the gate and lets generation 3 build.
4336        tracker.unblock_drop();
4337        drop_reader.await.unwrap();
4338        wait_for_group_generation(&group, 3).await;
4339        assert_eq!(tracker.builds.load(Relaxed), 2);
4340    }
4341
4342    #[tokio::test(flavor = "current_thread")]
4343    async fn test_retired_selector_destruction_does_not_block_runtime() {
4344        let b1 = Backend::new("1.0.0.1:80").unwrap();
4345        let b2 = Backend::new("1.1.1.1:80").unwrap();
4346        let discovery = Arc::new(MutableDiscovery::new(BTreeSet::from([b1])));
4347        let tracker = Arc::new(BuildTracker::default());
4348        let group = LoadBalancerGroup::<TestSelection>::from_backends_with_configs(
4349            Backends::new(Box::new(Arc::clone(&discovery))),
4350            [Some(TestSelectionConfig {
4351                reverse: false,
4352                tracker: Some(Arc::clone(&tracker)),
4353            })],
4354        );
4355
4356        group.update().await.unwrap();
4357        wait_for_group_generation(&group, 1).await;
4358        tracker.block_drop();
4359
4360        let (release_tx, release_rx) = std::sync::mpsc::channel();
4361        let release_tracker = Arc::clone(&tracker);
4362        let release_thread = std::thread::spawn(move || {
4363            let _ = release_rx.recv_timeout(Duration::from_secs(1));
4364            release_tracker.unblock_drop();
4365        });
4366
4367        let start = Instant::now();
4368        discovery.add(b2);
4369        group.update().await.unwrap();
4370        wait_for_group_generation(&group, 2).await;
4371        let publish_duration = start.elapsed();
4372
4373        let _ = release_tx.send(());
4374        release_thread.join().unwrap();
4375        assert!(
4376            publish_duration < Duration::from_millis(500),
4377            "selector publication waited for retired selector destruction: {publish_duration:?}"
4378        );
4379    }
4380
4381    // A private registry keys health by full backend identity, so two backends
4382    // sharing an address but differing in weight retain independent health.
4383    #[tokio::test]
4384    async fn test_private_registry_preserves_backend_identity() {
4385        let healthy = Backend::new_with_weight("1.0.0.1:80", 1).unwrap();
4386        let unhealthy = Backend::new_with_weight("1.0.0.1:80", 2).unwrap();
4387        let discovery = Arc::new(MutableDiscovery::new(BTreeSet::from([
4388            healthy.clone(),
4389            unhealthy.clone(),
4390        ])));
4391        let checks = Arc::new(AtomicUsize::new(0));
4392        let mut backends = Backends::new(Box::new(discovery));
4393        backends.set_health_check(Box::new(WeightHealthCheck {
4394            checks: Arc::clone(&checks),
4395            unhealthy_weight: 2,
4396        }));
4397
4398        backends.update(|_| {}).await.unwrap();
4399        backends.run_health_check(false).await;
4400
4401        // Each identity is probed independently.
4402        assert_eq!(checks.load(Relaxed), 2);
4403        assert!(backends.ready(&healthy));
4404        assert!(!backends.ready(&unhealthy));
4405    }
4406
4407    mod thread_safety {
4408        use super::*;
4409
4410        struct MockDiscovery {
4411            expected: usize,
4412        }
4413        #[async_trait]
4414        impl ServiceDiscovery for MockDiscovery {
4415            async fn discover(&self) -> Result<(BTreeSet<Backend>, HashMap<u64, bool>)> {
4416                let mut d = BTreeSet::new();
4417                let mut m = HashMap::with_capacity(self.expected);
4418                for i in 0..self.expected {
4419                    let b = Backend::new(&format!("1.1.1.1:{i}")).unwrap();
4420                    m.insert(b.hash_key(), true);
4421                    d.insert(b);
4422                }
4423                Ok((d, m))
4424            }
4425        }
4426
4427        #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
4428        async fn test_consistency() {
4429            let expected = 3000;
4430            let discovery = MockDiscovery { expected };
4431            let lb = Arc::new(LoadBalancer::<selection::Consistent>::from_backends(
4432                Backends::new(Box::new(discovery)),
4433            ));
4434            let lb2 = lb.clone();
4435
4436            tokio::spawn(async move {
4437                assert!(lb2.update().await.is_ok());
4438            });
4439            let mut backend_count = 0;
4440            while backend_count == 0 {
4441                let backends = lb.backends();
4442                backend_count = backends.get_backend().len();
4443            }
4444            assert_eq!(backend_count, expected);
4445            assert!(lb.select_with(b"test", 1, |_, _| true).is_some());
4446        }
4447    }
4448}