1#![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#[derive(Derivative)]
171#[derivative(Clone, Hash, PartialEq, PartialOrd, Eq, Ord, Debug)]
172pub struct Backend {
173 pub addr: SocketAddr,
175 pub weight: usize,
178
179 #[derivative(PartialEq = "ignore")]
186 #[derivative(PartialOrd = "ignore")]
187 #[derivative(Hash = "ignore")]
188 #[derivative(Ord = "ignore")]
189 pub ext: Extensions,
190}
191
192impl Backend {
193 pub fn new(addr: &str) -> Result<Self> {
196 Self::new_with_weight(addr, 1)
197 }
198
199 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 }
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
242struct HealthRegistryState {
247 targets: Box<[Backend]>,
248 health: HashMap<u64, Health>,
253}
254
255pub struct HealthRegistry {
260 health_check: OnceLock<Arc<dyn health_check::HealthCheck + Send + Sync + 'static>>,
261 state: ArcSwap<HealthRegistryState>,
262 views: Mutex<BTreeMap<u64, Arc<BTreeSet<Backend>>>>,
265 next_view_id: AtomicU64,
267 view_removal_tx: mpsc::Sender<u64>,
269 pending_view_removals: Mutex<mpsc::Receiver<u64>>,
271 view_removed: Notify,
273 targets_available: Notify,
275 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 pub fn new() -> Self {
288 Self::with_equivalence(|backend| backend.hash_key())
289 }
290
291 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 fn health_key(&self, backend: &Backend) -> u64 {
318 (self.equivalence_key)(backend)
319 }
320
321 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 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 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 pub fn target_count(&self) -> usize {
433 self.apply_pending_view_removals();
434 self.state.load().targets.len()
435 }
436
437 pub(crate) async fn wait_for_targets(&self) {
439 loop {
440 let notified = self.targets_available.notified();
441 tokio::pin!(notified);
444 notified.as_mut().enable();
445 if self.target_count() > 0 {
446 return;
447 }
448 notified.await;
449 }
450 }
451
452 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 return;
487 };
488
489 let state = self.state.load_full();
490 if parallel {
491 let runtime = current_handle();
492 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 while let Some(joined) = jobs.join_next().await {
514 if let Err(error) = joined {
515 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#[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#[derive(Clone)]
557struct ReadinessSnapshot(Arc<HashMap<u64, BackendReadiness>>);
558
559impl ReadinessSnapshot {
560 fn empty() -> Self {
562 Self(Arc::new(HashMap::new()))
563 }
564
565 fn get(&self, backend: &Backend) -> Option<&BackendReadiness> {
567 self.0.get(&backend.hash_key())
568 }
569
570 fn ready(&self, backend: &Backend) -> bool {
573 self.get(backend).is_some_and(BackendReadiness::ready)
574 }
575}
576
577struct BackendViewState {
580 backends: Arc<BTreeSet<Backend>>,
582 readiness: ReadinessSnapshot,
584}
585
586struct BackendUpdate {
594 generation: u64,
596 backends: Arc<BTreeSet<Backend>>,
598 readiness: ReadinessSnapshot,
600}
601
602pub struct Backends {
608 discovery: Box<dyn ServiceDiscovery + Send + Sync + 'static>,
609 health_registry: Arc<HealthRegistry>,
610 view_id: u64,
611 state: ArcSwap<BackendViewState>,
612 generation: AtomicU64,
614 enablement_handles: Mutex<HashMap<u64, Weak<AtomicBool>>>,
623 owns_health_checks: bool,
626}
627
628pub type BackendView = Backends;
630
631impl Backends {
632 pub fn new(discovery: Box<dyn ServiceDiscovery + Send + Sync + 'static>) -> Self {
637 Self::new_inner(discovery, Arc::new(HealthRegistry::new()), true)
638 }
639
640 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 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 fn readiness_snapshot(&self) -> ReadinessSnapshot {
686 self.state.load().readiness.clone()
687 }
688
689 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 handles.retain(|_, weak| weak.strong_count() > 0);
721 for backend in new_backends.iter() {
722 let key = backend.hash_key();
723 let enabled = old_state
726 .readiness
727 .get(backend)
728 .map(|current| Arc::clone(¤t.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 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 pub fn ready(&self, backend: &Backend) -> bool {
791 self.state.load().readiness.ready(backend)
792 }
793
794 pub fn set_enable(&self, backend: &Backend, enabled: bool) {
803 let key = backend.hash_key();
804 if let Some(readiness) = self.state.load().readiness.0.get(&key) {
807 readiness.enabled.store(enabled, Relaxed);
808 return;
809 }
810 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 pub fn get_backend(&self) -> Arc<BTreeSet<Backend>> {
823 Arc::clone(&self.state.load().backends)
824 }
825
826 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 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 pub async fn run_health_check(&self, parallel: bool) {
850 self.health_registry.run_health_check(parallel).await;
851 }
852
853 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
865pub struct HealthCheckService {
881 registry: Arc<HealthRegistry>,
882 pub health_check_frequency: Option<Duration>,
886 pub parallel_health_check: bool,
888}
889
890impl HealthCheckService {
891 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#[derive(Debug, Clone, Copy)]
906pub struct UpdateTimings {
907 pub discovery_duration: Duration,
909 pub build_duration: Duration,
916}
917
918pub struct LoadBalancer<S>
924where
925 S: BackendSelection,
926{
927 backends: Backends,
928 selector: ArcSwap<S>,
929
930 config: Option<S::Config>,
931
932 last_update_timing: ArcSwap<Option<UpdateTimings>>,
936
937 pub health_check_frequency: Option<Duration>,
944 pub update_frequency: Option<Duration>,
948 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 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 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 pub fn from_backends(backends: Backends) -> Self {
1005 Self::from_backends_with_config(backends, None)
1006 }
1007
1008 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 pub fn select(&self, key: &[u8], max_iterations: usize) -> Option<Backend> {
1052 self.select_with(key, max_iterations, |_, health| health)
1053 }
1054
1055 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 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 pub fn backends(&self) -> &Backends {
1086 &self.backends
1087 }
1088
1089 pub fn last_update_timing(&self) -> Option<UpdateTimings> {
1093 **self.last_update_timing.load()
1094 }
1095}
1096
1097#[derive(Debug, Clone, Copy)]
1099pub struct SelectorUpdateTimings {
1100 pub generation: u64,
1102 pub queue_duration: Duration,
1104 pub build_duration: Duration,
1106}
1107
1108struct SelectorRebuildRequest {
1110 generation: u64,
1112 backends: Arc<BTreeSet<Backend>>,
1114 readiness: ReadinessSnapshot,
1118 requested_at: Instant,
1120}
1121
1122#[derive(Default)]
1124struct SelectorRebuildState {
1125 is_running: bool,
1127 pending_request: Option<SelectorRebuildRequest>,
1129}
1130
1131struct SelectorRebuildTaskGuard<S>
1133where
1134 S: BackendSelection,
1135{
1136 slot: Arc<SelectorSlot<S>>,
1138 rebuild_notify: Arc<Notify>,
1140 in_flight: Option<SelectorRebuildRequest>,
1142 armed: bool,
1144}
1145
1146struct SelectorRebuildCancellation {
1151 cancelled: AtomicBool,
1153 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 if !self.armed {
1214 return;
1215 }
1216
1217 if std::thread::panicking() {
1219 self.slot.failed_rebuilds.fetch_add(1, Relaxed);
1220 log::error!("load-balancing selector rebuild task panicked");
1221 }
1222 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 rebuild_state.is_running = false;
1236 drop(rebuild_state);
1237 self.rebuild_notify.notify_one();
1240 }
1241}
1242
1243struct SelectorSlot<S>
1245where
1246 S: BackendSelection,
1247{
1248 selector: ArcSwap<PublishedSelector<S>>,
1250 config: Option<S::Config>,
1252 generation: AtomicU64,
1254 rebuild_state: Mutex<SelectorRebuildState>,
1256 last_update_timing: ArcSwap<Option<SelectorUpdateTimings>>,
1258 coalesced_rebuilds: AtomicU64,
1260 failed_rebuilds: AtomicU64,
1262 interrupt_retry: Notify,
1264}
1265
1266struct SelectorReleaseSignal {
1268 released: AtomicBool,
1270 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
1295struct SelectorReleaseGuard {
1303 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
1314struct PublishedSelector<S> {
1321 selector: Arc<S>,
1325 readiness: ReadinessSnapshot,
1327 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
1347pub struct SelectorRebuildGate {
1360 semaphore: Arc<Semaphore>,
1362 retired: Mutex<Option<Arc<SelectorReleaseSignal>>>,
1364}
1365
1366impl Default for SelectorRebuildGate {
1367 fn default() -> Self {
1368 Self::new()
1369 }
1370}
1371
1372impl SelectorRebuildGate {
1373 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 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 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
1423pub struct LoadBalancerGroup<S>
1438where
1439 S: BackendSelection,
1440{
1441 backends: Backends,
1443 selectors: Box<[Arc<SelectorSlot<S>>]>,
1445 rebuild_gate: Arc<SelectorRebuildGate>,
1447 rebuild_notify: Arc<Notify>,
1449 rebuild_cancellation: Arc<SelectorRebuildCancellation>,
1451
1452 last_update_timing: ArcSwap<Option<UpdateTimings>>,
1456
1457 pub health_check_frequency: Option<Duration>,
1464 pub update_frequency: Option<Duration>,
1468 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
1478const INITIAL_SELECTOR_REBUILD_BACKOFF: Duration = Duration::from_millis(10);
1486const 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 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 return;
1507 }
1508 {
1509 let mut rebuild_state = lock_rebuild_state(&slot.rebuild_state);
1510 if rebuild_state.pending_request.is_none() {
1511 rebuild_state.is_running = false;
1513 task_guard.disarm();
1514 return;
1515 }
1516 }
1517
1518 let permit = tokio::select! {
1519 permit = rebuild_gate.acquire() => permit,
1521 _ = rebuild_cancellation.wait_for_cancel() => return, };
1523
1524 let Some(request) = lock_rebuild_state(&slot.rebuild_state)
1525 .pending_request
1526 .take()
1527 else {
1528 drop(permit);
1530 continue;
1531 };
1532 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 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 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 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 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 backoff = (backoff * 2).min(MAX_SELECTOR_REBUILD_BACKOFF);
1629 }
1630 _ = slot.interrupt_retry.notified() => {
1631 backoff = INITIAL_SELECTOR_REBUILD_BACKOFF;
1634 }
1635 _ = rebuild_cancellation.wait_for_cancel() => return,
1637 }
1638 } else {
1639 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 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>(¤t_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 pub fn with_rebuild_gate(mut self, rebuild_gate: Arc<SelectorRebuildGate>) -> Self {
1714 self.rebuild_gate = rebuild_gate;
1715 self
1716 }
1717
1718 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 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 false
1748 } else {
1749 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 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 build_duration: Duration::ZERO,
1793 })));
1794
1795 Ok(())
1796 }
1797
1798 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 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 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 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 pub fn backends(&self) -> &Backends {
1854 &self.backends
1855 }
1856
1857 pub fn backend_generation(&self) -> u64 {
1859 self.backends.generation()
1860 }
1861
1862 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 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 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 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 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 pub(crate) fn rebuild_notified(&self) -> impl Future<Output = ()> + '_ {
1902 self.rebuild_notify.notified()
1903 }
1904
1905 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 block_from: AtomicUsize,
1936 panics: AtomicUsize,
1938 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 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 let updated = AtomicBool::new(false);
2320 backends
2321 .update(|_| updated.store(true, Relaxed))
2322 .await
2323 .unwrap();
2324 assert!(updated.load(Relaxed));
2325
2326 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 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 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 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 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 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 panicked.notified().await;
2501 release.notify_one();
2502
2503 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 assert!(lb.last_update_timing().is_none());
2530
2531 lb.update().await.unwrap();
2532
2533 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 let backend = lb.backends().get_backend();
2542 assert!(backend.contains(&b1));
2543 assert!(backend.contains(&b2));
2544
2545 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 ®istry_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(®istry),
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(®istry),
2740 );
2741 let second = Backends::new_with_health_registry(
2742 Box::new(Arc::clone(&second_discovery)),
2743 Arc::clone(®istry),
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(®istry),
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(®istry),
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(®istry),
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 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 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(®istry));
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(®istry));
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(®istry));
3042 view.update(|_| {}).await.unwrap();
3043
3044 let mut service = HealthCheckService::new(Arc::clone(®istry));
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(®istry),
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(®istry),
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 #[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(®istry),
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 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 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 #[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 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 assert_eq!(group.select(0, b"", 10), Some(old.clone()));
3201 assert!(!group.backends().ready(&old));
3203 assert!(group.backends().ready(&new));
3204
3205 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 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 #[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 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 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 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 group.backends().set_enable(&w100, false);
3268 assert_eq!(group.select(0, b"", 10), None);
3269
3270 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 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 assert!(group.backends().ready(&w102));
3284 assert_eq!(group.select(0, b"", 10), Some(w102));
3285 }
3286
3287 #[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 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 assert!(!group.backends().ready(&w100));
3323 assert_eq!(group.select(0, b"", 10), Some(w100.clone()));
3324 assert!(group.backends().ready(&moved));
3325
3326 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 #[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 let published = group.selectors[0].selector.load_full();
3358 assert!(published.readiness.ready(&old));
3359
3360 discovery.replace(BTreeSet::from([new.clone()]));
3362 group.update().await.unwrap();
3363 wait_for_group_generation(&group, 2).await;
3364
3365 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 assert_eq!(group.select(0, b"", 10), Some(new.clone()));
3374 assert!(group.backends().ready(&new));
3375
3376 drop(iter);
3378 drop(published);
3379 assert_eq!(group.select(0, b"", 10), Some(new));
3380 }
3381
3382 #[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 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 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 assert_eq!(group.select(0, b"", 10), Some(a.clone()));
3439 assert_eq!(group.select(1, b"", 10), Some(b.clone()));
3440
3441 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 #[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 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 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 #[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 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 #[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 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 #[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(®istry),
4020 );
4021 view.update(|_| {}).await.unwrap();
4022
4023 let service = HealthCheckService::new(Arc::clone(®istry));
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 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 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 #[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(®istry),
4069 );
4070 first.update(|_| {}).await.unwrap();
4071 assert!(first.ready(&backend));
4072
4073 registry.run_health_check(false).await;
4075 assert!(!first.ready(&backend));
4076
4077 let second = Backends::new_with_health_registry(
4080 Box::new(Arc::new(MutableDiscovery::new(BTreeSet::from([
4081 other.clone()
4082 ])))),
4083 Arc::clone(®istry),
4084 );
4085 second.update(|_| {}).await.unwrap();
4086 assert!(!first.ready(&backend));
4087
4088 first_discovery.add(other.clone());
4091 first.update(|_| {}).await.unwrap();
4092 assert!(!first.ready(&backend));
4093 assert!(first.ready(&other));
4094 }
4095
4096 #[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 tracker.set_panics(1);
4113 group.update().await.unwrap();
4114 assert_eq!(group.backend_generation(), 1);
4115
4116 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 #[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 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 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 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 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 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 #[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 let published = group.selectors[0].selector.load();
4304 let iter = published.selector.iter(b"");
4305 let reader = (iter, published);
4306
4307 tracker.block_drop();
4309
4310 discovery.add(b2);
4312 group.update().await.unwrap();
4313 wait_for_builds_started(&tracker, 1).await;
4314
4315 discovery.add(b3);
4317 group.update().await.unwrap();
4318
4319 tokio::time::sleep(Duration::from_millis(25)).await;
4322 assert_eq!(tracker.builds.load(Relaxed), 1);
4323
4324 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 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 #[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 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}