1use std::future::Future;
18use std::time::{Duration, Instant};
19
20use super::{BackendIter, BackendSelection, HealthCheckService, LoadBalancer, LoadBalancerGroup};
21use async_trait::async_trait;
22use pingora_core::{
23 server::ShutdownWatch,
24 services::{background::BackgroundService, ServiceReadyNotifier},
25};
26
27enum WatchChange {
29 ShuttingDown,
31 Spurious,
33 Closed,
39}
40
41async fn shutdown_changed(shutdown: &mut ShutdownWatch) -> WatchChange {
43 match shutdown.changed().await {
44 Ok(()) if *shutdown.borrow() => WatchChange::ShuttingDown,
47 Ok(()) => WatchChange::Spurious,
48 Err(_) => WatchChange::Closed,
49 }
50}
51
52enum Raced<T> {
57 Completed(T),
59 ShuttingDown,
62}
63
64async fn race_shutdown<F: Future>(shutdown: &mut ShutdownWatch, work: F) -> Raced<F::Output> {
75 tokio::pin!(work);
76 loop {
77 tokio::select! {
78 biased;
79 change = shutdown_changed(&mut *shutdown) => match change {
80 WatchChange::ShuttingDown => return Raced::ShuttingDown,
81 WatchChange::Spurious => continue,
82 WatchChange::Closed => break,
85 },
86 output = &mut work => return Raced::Completed(output),
87 }
88 }
89 Raced::Completed(work.await)
90}
91
92struct TaskSchedule {
94 frequency: Option<Duration>,
95 next: Option<Instant>,
96}
97
98struct BackgroundSchedule {
100 update: Option<TaskSchedule>,
101 health_check: Option<TaskSchedule>,
102}
103
104impl TaskSchedule {
105 fn new(frequency: Option<Duration>, now: Instant) -> Self {
106 Self {
107 frequency,
108 next: Some(now),
109 }
110 }
111
112 fn is_due(&self, now: Instant) -> bool {
113 self.next.is_some_and(|next| next <= now)
114 }
115
116 fn finished_at(&mut self, base: Instant) {
117 self.next = self.frequency.map(|frequency| base + frequency);
118 }
119
120 fn next(&self) -> Option<Instant> {
121 self.next
122 }
123
124 fn is_once(&self) -> bool {
125 self.frequency.is_none()
126 }
127}
128
129impl BackgroundSchedule {
130 const NEVER: Duration = Duration::from_secs(u32::MAX as u64);
132
133 fn new(
134 update_frequency: Option<Option<Duration>>,
135 health_check_frequency: Option<Option<Duration>>,
136 now: Instant,
137 ) -> Self {
138 Self {
139 update: update_frequency.map(|frequency| TaskSchedule::new(frequency, now)),
140 health_check: health_check_frequency.map(|frequency| TaskSchedule::new(frequency, now)),
141 }
142 }
143
144 fn update_is_due(&self, now: Instant) -> bool {
145 self.update
146 .as_ref()
147 .is_some_and(|schedule| schedule.is_due(now))
148 }
149
150 fn health_check_is_due(&self, now: Instant) -> bool {
151 self.health_check
152 .as_ref()
153 .is_some_and(|schedule| schedule.is_due(now))
154 }
155
156 fn update_finished_at(&mut self, base: Instant) {
157 if let Some(schedule) = self.update.as_mut() {
158 schedule.finished_at(base);
159 }
160 }
161
162 fn health_check_finished_at(&mut self, base: Instant) {
163 if let Some(schedule) = self.health_check.as_mut() {
164 schedule.finished_at(base);
165 }
166 }
167
168 fn next_scheduled(&self) -> Option<Instant> {
169 self.update
170 .iter()
171 .chain(self.health_check.iter())
172 .filter_map(TaskSchedule::next)
173 .min()
174 }
175
176 fn next(&self, now: Instant) -> Instant {
177 self.next_scheduled().unwrap_or(now + Self::NEVER)
178 }
179
180 fn next_health_check(&self) -> Option<Instant> {
181 self.health_check.as_ref().and_then(TaskSchedule::next)
182 }
183
184 fn health_check_is_once(&self) -> bool {
185 self.health_check
186 .as_ref()
187 .is_some_and(TaskSchedule::is_once)
188 }
189
190 fn is_idle(&self) -> bool {
191 self.next_scheduled().is_none()
192 }
193}
194
195impl<S: Send + Sync + BackendSelection + 'static> LoadBalancer<S>
196where
197 S::Iter: BackendIter,
198{
199 pub async fn run(
200 &self,
201 mut shutdown: ShutdownWatch,
202 mut ready_opt: Option<ServiceReadyNotifier>,
203 ) -> () {
204 let mut now = Instant::now();
205 let mut schedule = BackgroundSchedule::new(
206 Some(self.update_frequency),
207 self.backends
210 .owns_health_checks()
211 .then_some(self.health_check_frequency),
212 now,
213 );
214 loop {
215 if *shutdown.borrow() {
216 return;
217 }
218
219 if schedule.update_is_due(now) {
220 match race_shutdown(&mut shutdown, self.update()).await {
222 Raced::ShuttingDown => return,
223 Raced::Completed(_) => schedule.update_finished_at(now),
224 }
225 }
226
227 if let Some(ready) = ready_opt.take() {
230 ServiceReadyNotifier::notify_ready(ready)
231 }
232
233 if schedule.health_check_is_due(now) {
234 let health_check = self.backends.run_health_check(self.parallel_health_check);
235 match race_shutdown(&mut shutdown, health_check).await {
236 Raced::ShuttingDown => return,
237 Raced::Completed(()) => schedule.health_check_finished_at(now),
238 }
239 }
240
241 if schedule.is_idle() {
242 return;
243 }
244 let to_wake = schedule.next(now);
245 match race_shutdown(&mut shutdown, tokio::time::sleep_until(to_wake.into())).await {
246 Raced::ShuttingDown => return,
247 Raced::Completed(()) => {}
248 }
249 now = Instant::now();
250 }
251 }
252}
253
254#[async_trait]
258impl<S: Send + Sync + BackendSelection + 'static> BackgroundService for LoadBalancer<S>
259where
260 S::Iter: BackendIter,
261{
262 async fn start_with_ready_notifier(
263 &self,
264 shutdown: pingora_core::server::ShutdownWatch,
265 ready: ServiceReadyNotifier,
266 ) -> () {
267 self.run(shutdown, Some(ready)).await
268 }
269
270 async fn start(&self, shutdown: pingora_core::server::ShutdownWatch) -> () {
271 self.run(shutdown, None).await
272 }
273}
274
275impl<S: Send + Sync + BackendSelection + 'static> LoadBalancerGroup<S>
276where
277 S::Config: 'static,
278 S::Iter: BackendIter,
279{
280 pub async fn run(
283 &self,
284 mut shutdown: pingora_core::server::ShutdownWatch,
285 mut ready_opt: Option<ServiceReadyNotifier>,
286 ) {
287 let mut now = Instant::now();
288 let mut schedule = BackgroundSchedule::new(
289 Some(self.update_frequency),
290 self.backends()
293 .owns_health_checks()
294 .then_some(self.health_check_frequency),
295 now,
296 );
297 let mut ready_generation = None;
298
299 loop {
300 if *shutdown.borrow() {
301 return;
302 }
303
304 if schedule.update_is_due(now) {
305 match race_shutdown(&mut shutdown, self.update()).await {
306 Raced::ShuttingDown => return,
307 Raced::Completed(Ok(())) => {
308 if ready_opt.is_some() {
314 ready_generation = Some(self.backend_generation());
315 }
316 schedule.update_finished_at(now);
317 }
318 Raced::Completed(Err(error)) => {
319 log::error!("load balancer group update failed: {error}");
320 schedule.update_finished_at(now);
321 }
322 }
323 }
324
325 if ready_generation.is_some_and(|generation| self.selectors_ready_for(generation)) {
326 if let Some(ready) = ready_opt.take() {
327 ServiceReadyNotifier::notify_ready(ready)
330 }
331 }
332
333 if schedule.health_check_is_due(now) {
334 let health_check = self.backends().run_health_check(self.parallel_health_check);
335 match race_shutdown(&mut shutdown, health_check).await {
336 Raced::ShuttingDown => return,
337 Raced::Completed(()) => schedule.health_check_finished_at(now),
338 }
339 }
340
341 if ready_opt.is_none() && schedule.is_idle() {
344 return;
347 }
348
349 let to_wake = schedule.next(now);
350 let wake = async {
353 tokio::select! {
354 _ = tokio::time::sleep_until(to_wake.into()) => {}
355 _ = self.rebuild_notified(), if ready_opt.is_some() => {} }
357 };
358 match race_shutdown(&mut shutdown, wake).await {
359 Raced::ShuttingDown => return,
360 Raced::Completed(()) => {}
361 }
362 now = Instant::now();
363 }
364 }
365}
366
367#[async_trait]
368impl<S: Send + Sync + BackendSelection + 'static> BackgroundService for LoadBalancerGroup<S>
369where
370 S::Config: 'static,
371 S::Iter: BackendIter,
372{
373 async fn start_with_ready_notifier(
374 &self,
375 shutdown: pingora_core::server::ShutdownWatch,
376 ready: ServiceReadyNotifier,
377 ) {
378 self.run(shutdown, Some(ready)).await
379 }
380
381 async fn start(&self, shutdown: pingora_core::server::ShutdownWatch) {
382 self.run(shutdown, None).await
383 }
384}
385
386impl HealthCheckService {
387 pub async fn run(
389 &self,
390 mut shutdown: pingora_core::server::ShutdownWatch,
391 mut ready_opt: Option<ServiceReadyNotifier>,
392 ) {
393 if !self.registry.has_health_check() {
394 log::error!("HealthCheckService requires a configured HealthRegistry health check");
395 if ready_opt.is_some() && !*shutdown.borrow() {
400 while let WatchChange::Spurious = shutdown_changed(&mut shutdown).await {}
401 }
402 return;
403 }
404
405 let mut schedule =
406 BackgroundSchedule::new(None, Some(self.health_check_frequency), Instant::now());
407 loop {
408 if *shutdown.borrow() {
409 return;
410 }
411
412 if schedule.health_check_is_once() && self.registry.target_count() == 0 {
418 match race_shutdown(&mut shutdown, self.registry.wait_for_targets()).await {
419 Raced::ShuttingDown => return,
420 Raced::Completed(()) => {}
421 }
422 if *shutdown.borrow() {
426 return;
427 }
428 }
429
430 let health_check = self.registry.run_health_check(self.parallel_health_check);
431 match race_shutdown(&mut shutdown, health_check).await {
432 Raced::ShuttingDown => return,
433 Raced::Completed(()) => {
434 if let Some(ready) = ready_opt.take() {
435 ServiceReadyNotifier::notify_ready(ready);
438 }
439 schedule.health_check_finished_at(Instant::now());
440 }
441 }
442
443 let Some(next_health_check) = schedule.next_health_check() else {
444 return;
446 };
447 loop {
448 let has_targets = self.registry.target_count() > 0;
449 let wake = async {
452 tokio::select! {
453 _ = self.registry.wait_for_targets(), if !has_targets => true,
454 _ = tokio::time::sleep_until(next_health_check.into()), if has_targets => true,
455 _ = self.registry.wait_for_view_removal() => false,
456 }
457 };
458 match race_shutdown(&mut shutdown, wake).await {
459 Raced::ShuttingDown => return,
460 Raced::Completed(true) => break,
461 Raced::Completed(false) => {}
462 }
463 }
464 }
465 }
466}
467
468#[async_trait]
469impl BackgroundService for HealthCheckService {
470 async fn start_with_ready_notifier(
471 &self,
472 shutdown: pingora_core::server::ShutdownWatch,
473 ready: ServiceReadyNotifier,
474 ) {
475 self.run(shutdown, Some(ready)).await
476 }
477
478 async fn start(&self, shutdown: pingora_core::server::ShutdownWatch) {
479 self.run(shutdown, None).await
480 }
481}
482
483#[cfg(test)]
484mod tests {
485 use std::collections::{BTreeSet, HashMap};
486 use std::future;
487 use std::sync::Arc;
488
489 use async_trait::async_trait;
490 use pingora_error::Result;
491 use tokio::sync::{watch, Notify};
492
493 use super::*;
494 use crate::discovery::{ServiceDiscovery, Static};
495 use crate::health_check::HealthCheck;
496 use crate::selection;
497 use crate::{Backend, Backends, HealthRegistry};
498
499 #[test]
500 fn schedule_without_tasks_is_idle() {
501 let now = Instant::now();
502 let mut schedule = BackgroundSchedule::new(None, None, now);
503
504 assert!(!schedule.update_is_due(now));
505 assert!(!schedule.health_check_is_due(now));
506 schedule.update_finished_at(now);
507 schedule.health_check_finished_at(now);
508 assert!(schedule.is_idle());
509 assert_eq!(schedule.next(now), now + BackgroundSchedule::NEVER);
510 assert_eq!(schedule.next_health_check(), None);
511 assert!(!schedule.health_check_is_once());
512 }
513
514 #[test]
515 fn one_time_update_schedule_finishes_after_first_run() {
516 let now = Instant::now();
517 let mut schedule = BackgroundSchedule::new(Some(None), None, now);
518
519 assert!(schedule.update_is_due(now));
520 assert!(!schedule.is_idle());
521 schedule.update_finished_at(now);
522 assert!(!schedule.update_is_due(now));
523 assert!(schedule.is_idle());
524 assert_eq!(schedule.next(now), now + BackgroundSchedule::NEVER);
525 }
526
527 #[test]
528 fn periodic_health_check_schedule_tracks_next_pass() {
529 let now = Instant::now();
530 let frequency = Duration::from_secs(5);
531 let mut schedule = BackgroundSchedule::new(None, Some(Some(frequency)), now);
532
533 assert!(schedule.health_check_is_due(now));
534 assert!(!schedule.health_check_is_once());
535 schedule.health_check_finished_at(now);
536 assert!(!schedule.health_check_is_due(now));
537 assert!(schedule.health_check_is_due(now + frequency));
538 assert!(!schedule.is_idle());
539 assert_eq!(schedule.next_health_check(), Some(now + frequency));
540 }
541
542 #[test]
543 fn one_time_health_check_schedule_finishes_after_first_run() {
544 let now = Instant::now();
545 let mut schedule = BackgroundSchedule::new(None, Some(None), now);
546
547 assert!(schedule.health_check_is_once());
548 assert!(schedule.health_check_is_due(now));
549 schedule.health_check_finished_at(now);
550 assert!(!schedule.health_check_is_due(now));
551 assert_eq!(schedule.next_health_check(), None);
552 assert!(schedule.is_idle());
553 assert_eq!(schedule.next(now), now + BackgroundSchedule::NEVER);
554 }
555
556 #[test]
557 fn schedule_uses_earliest_task_deadline() {
558 let now = Instant::now();
559 let update_frequency = Duration::from_secs(10);
560 let health_check_frequency = Duration::from_secs(5);
561 let mut schedule = BackgroundSchedule::new(
562 Some(Some(update_frequency)),
563 Some(Some(health_check_frequency)),
564 now,
565 );
566
567 schedule.update_finished_at(now);
568 schedule.health_check_finished_at(now);
569 assert_eq!(schedule.next(now), now + health_check_frequency);
570 assert!(!schedule.update_is_due(now + health_check_frequency));
571 assert!(schedule.health_check_is_due(now + health_check_frequency));
572 assert!(schedule.update_is_due(now + update_frequency));
573 }
574
575 struct NotifyingDiscovery {
576 notify: Arc<Notify>,
577 }
578
579 #[async_trait]
580 impl ServiceDiscovery for NotifyingDiscovery {
581 async fn discover(&self) -> Result<(BTreeSet<Backend>, HashMap<u64, bool>)> {
582 self.notify.notify_one();
583 Ok((BTreeSet::new(), HashMap::new()))
584 }
585 }
586
587 struct PendingDiscovery {
588 notify: Arc<Notify>,
589 }
590
591 #[async_trait]
592 impl ServiceDiscovery for PendingDiscovery {
593 async fn discover(&self) -> Result<(BTreeSet<Backend>, HashMap<u64, bool>)> {
594 self.notify.notify_one();
595 future::pending().await
596 }
597 }
598
599 struct PendingHealthCheck {
600 notify: Arc<Notify>,
601 drop_notify: Arc<Notify>,
602 }
603
604 #[async_trait]
605 impl HealthCheck for PendingHealthCheck {
606 async fn check(&self, _target: &Backend) -> Result<()> {
607 struct NotifyOnDrop(Arc<Notify>);
608
609 impl Drop for NotifyOnDrop {
610 fn drop(&mut self) {
611 self.0.notify_one();
612 }
613 }
614
615 let _notify_on_drop = NotifyOnDrop(self.drop_notify.clone());
616 self.notify.notify_one();
617 future::pending().await
618 }
619
620 fn health_threshold(&self, _success: bool) -> usize {
621 1
622 }
623 }
624
625 async fn assert_run_exits_on_shutdown(
626 lb: LoadBalancer<selection::RoundRobin>,
627 notify: Arc<Notify>,
628 ) {
629 let (shutdown_tx, shutdown_rx) = watch::channel(false);
630 let handle = tokio::spawn(async move {
631 lb.run(shutdown_rx, None).await;
632 });
633
634 notify.notified().await;
635 shutdown_tx.send(true).unwrap();
636
637 tokio::time::timeout(Duration::from_secs(1), handle)
638 .await
639 .expect("background service should observe shutdown promptly")
640 .expect("background service task should not panic");
641 }
642
643 #[tokio::test]
644 async fn run_returns_when_shutdown_while_sleeping() {
645 let notify = Arc::new(Notify::new());
646 let discovery = NotifyingDiscovery {
647 notify: notify.clone(),
648 };
649 let mut lb = LoadBalancer::<selection::RoundRobin>::from_backends(Backends::new(Box::new(
650 discovery,
651 )));
652 lb.update_frequency = Some(Duration::from_secs(60));
653
654 assert_run_exits_on_shutdown(lb, notify).await;
655 }
656
657 #[tokio::test]
658 async fn run_updates_when_no_shutdown_sender_remains() {
659 let notify = Arc::new(Notify::new());
660 let discovery = NotifyingDiscovery {
661 notify: notify.clone(),
662 };
663 let mut lb = LoadBalancer::<selection::RoundRobin>::from_backends(Backends::new(Box::new(
664 discovery,
665 )));
666 lb.update_frequency = Some(Duration::from_secs(60));
667
668 let (shutdown_tx, shutdown_rx) = watch::channel(false);
669 drop(shutdown_tx);
673
674 let handle = tokio::spawn(async move { lb.run(shutdown_rx, None).await });
675
676 tokio::time::timeout(Duration::from_secs(1), notify.notified())
677 .await
678 .expect("discovery should still run with no shutdown sender left");
679 handle.abort();
680 }
681
682 #[tokio::test]
683 async fn run_continues_after_a_non_shutdown_watch_change() {
684 let notify = Arc::new(Notify::new());
685 let discovery = NotifyingDiscovery {
686 notify: notify.clone(),
687 };
688 let mut lb = LoadBalancer::<selection::RoundRobin>::from_backends(Backends::new(Box::new(
689 discovery,
690 )));
691 lb.update_frequency = Some(Duration::from_secs(60));
692
693 let (shutdown_tx, shutdown_rx) = watch::channel(false);
694 let handle = tokio::spawn(async move { lb.run(shutdown_rx, None).await });
695
696 notify.notified().await;
698 shutdown_tx.send(false).unwrap();
699
700 tokio::time::sleep(Duration::from_millis(50)).await;
702 assert!(
703 !handle.is_finished(),
704 "a non-shutdown watch change stopped the service"
705 );
706
707 shutdown_tx.send(true).unwrap();
708 tokio::time::timeout(Duration::from_secs(1), handle)
709 .await
710 .expect("a real shutdown should still stop the service")
711 .expect("background service task should not panic");
712 }
713
714 #[tokio::test]
715 async fn run_returns_when_shutdown_while_updating() {
716 let notify = Arc::new(Notify::new());
717 let discovery = PendingDiscovery {
718 notify: notify.clone(),
719 };
720 let lb = LoadBalancer::<selection::RoundRobin>::from_backends(Backends::new(Box::new(
721 discovery,
722 )));
723
724 assert_run_exits_on_shutdown(lb, notify).await;
725 }
726
727 #[tokio::test]
728 async fn run_returns_when_shutdown_while_health_checking() {
729 let notify = Arc::new(Notify::new());
730 let drop_notify = Arc::new(Notify::new());
731 let mut lb =
732 LoadBalancer::<selection::RoundRobin>::try_from_iter(["127.0.0.1:80"]).unwrap();
733 lb.set_health_check(Box::new(PendingHealthCheck {
734 notify: notify.clone(),
735 drop_notify: drop_notify.clone(),
736 }));
737
738 assert_run_exits_on_shutdown(lb, notify).await;
739 tokio::time::timeout(Duration::from_secs(1), drop_notify.notified())
740 .await
741 .expect("pending health check should be cancelled");
742 }
743
744 #[tokio::test]
745 async fn run_aborts_parallel_health_check_on_shutdown() {
746 let notify = Arc::new(Notify::new());
747 let drop_notify = Arc::new(Notify::new());
748 let mut lb =
749 LoadBalancer::<selection::RoundRobin>::try_from_iter(["127.0.0.1:80"]).unwrap();
750 lb.parallel_health_check = true;
751 lb.set_health_check(Box::new(PendingHealthCheck {
752 notify: notify.clone(),
753 drop_notify: drop_notify.clone(),
754 }));
755
756 assert_run_exits_on_shutdown(lb, notify).await;
757 tokio::time::timeout(Duration::from_secs(1), drop_notify.notified())
758 .await
759 .expect("parallel health check task should be aborted");
760 }
761
762 async fn assert_group_run_exits_on_shutdown(
763 group: LoadBalancerGroup<selection::RoundRobin>,
764 notify: Arc<Notify>,
765 ) {
766 let (shutdown_tx, shutdown_rx) = watch::channel(false);
767 let handle = tokio::spawn(async move {
768 group.run(shutdown_rx, None).await;
769 });
770
771 notify.notified().await;
772 shutdown_tx.send(true).unwrap();
773
774 tokio::time::timeout(Duration::from_secs(1), handle)
775 .await
776 .expect("background service should observe shutdown promptly")
777 .expect("background service task should not panic");
778 }
779
780 #[tokio::test]
781 async fn group_run_returns_when_shutdown_while_updating() {
782 let notify = Arc::new(Notify::new());
783 let discovery = PendingDiscovery {
784 notify: notify.clone(),
785 };
786 let group = LoadBalancerGroup::<selection::RoundRobin>::from_backends_with_configs(
787 Backends::new(Box::new(discovery)),
788 [None],
789 );
790
791 assert_group_run_exits_on_shutdown(group, notify).await;
792 }
793
794 #[tokio::test]
795 async fn group_run_returns_when_shutdown_while_health_checking() {
796 let notify = Arc::new(Notify::new());
797 let drop_notify = Arc::new(Notify::new());
798 let mut backends = Backends::new(Static::try_from_iter(["127.0.0.1:80"]).unwrap());
799 backends.set_health_check(Box::new(PendingHealthCheck {
800 notify: notify.clone(),
801 drop_notify: drop_notify.clone(),
802 }));
803 let group = LoadBalancerGroup::<selection::RoundRobin>::from_backends_with_configs(
804 backends,
805 [None],
806 );
807
808 assert_group_run_exits_on_shutdown(group, notify).await;
809 tokio::time::timeout(Duration::from_secs(1), drop_notify.notified())
810 .await
811 .expect("pending health check should be cancelled");
812 }
813
814 #[tokio::test]
815 async fn health_check_service_run_returns_when_shutdown_while_health_checking() {
816 let notify = Arc::new(Notify::new());
817 let drop_notify = Arc::new(Notify::new());
818 let registry = Arc::new(HealthRegistry::new());
819 registry.set_health_check(Box::new(PendingHealthCheck {
820 notify: notify.clone(),
821 drop_notify: drop_notify.clone(),
822 }));
823 let view = Backends::new_with_health_registry(
825 Static::try_from_iter(["127.0.0.1:80"]).unwrap(),
826 Arc::clone(®istry),
827 );
828 view.update(|_| {}).await.unwrap();
829
830 let service = HealthCheckService::new(registry);
831 let (shutdown_tx, shutdown_rx) = watch::channel(false);
832 let handle = tokio::spawn(async move { service.run(shutdown_rx, None).await });
833
834 notify.notified().await;
835 shutdown_tx.send(true).unwrap();
836
837 tokio::time::timeout(Duration::from_secs(1), handle)
838 .await
839 .expect("health check service should observe shutdown promptly")
840 .expect("health check service task should not panic");
841 tokio::time::timeout(Duration::from_secs(1), drop_notify.notified())
842 .await
843 .expect("pending health check should be cancelled");
844 }
845}