1use std::{
29 cell::RefCell,
30 collections::{HashMap, HashSet},
31 io::ErrorKind,
32 net::SocketAddr,
33 rc::{Rc, Weak},
34 time::{Duration, Instant},
35};
36
37use mio::{
38 Interest, Registry, Token,
39 net::{TcpStream, UdpSocket},
40};
41use sozu_command::{proto::command::UdpHealthConfig, state::ClusterId};
42
43use crate::backends::{Backend, BackendMap};
44use crate::metrics::names;
45use crate::socket::udp_connect;
46
47macro_rules! log_context {
49 () => {
50 "UDP-HEALTH"
51 };
52}
53
54const UDP_HEALTH_TOKEN_BASE: usize = (1 << 24) + (1 << 20);
58const UDP_HEALTH_TOKEN_CAPACITY: usize = 1 << 16;
60
61#[derive(Clone, Debug)]
65pub struct UdpHealthSettings {
66 pub tcp_port: Option<u16>,
68 pub rise: u32,
70 pub fall: u32,
72 pub interval: Duration,
74 pub timeout: Duration,
76 pub udp_probe_payload: Option<Vec<u8>>,
78}
79
80impl UdpHealthSettings {
81 pub fn from_proto(cfg: &UdpHealthConfig) -> Self {
84 UdpHealthSettings {
85 tcp_port: cfg.tcp_port.map(|p| p as u16),
86 rise: cfg.rise.unwrap_or(2),
87 fall: cfg.fall.unwrap_or(3),
88 interval: Duration::from_secs(u64::from(cfg.probe_interval_seconds.unwrap_or(5))),
89 timeout: Duration::from_secs(u64::from(cfg.probe_timeout_seconds.unwrap_or(2))),
90 udp_probe_payload: cfg.udp_probe_payload.clone(),
91 }
92 }
93}
94
95type ProbeBatch = Vec<(ClusterId, UdpHealthSettings, Vec<ProbeTarget>)>;
98
99type ProbeTarget = (SocketAddr, Weak<RefCell<Backend>>);
101
102enum ProbeSocket {
110 Tcp(TcpStream),
111 Udp(UdpSocket),
112}
113
114impl ProbeSocket {
115 fn deregister(&mut self, registry: &Registry) {
117 match self {
118 ProbeSocket::Tcp(s) => {
119 let _ = registry.deregister(s);
120 }
121 ProbeSocket::Udp(s) => {
122 let _ = registry.deregister(s);
123 }
124 }
125 }
126}
127
128struct InFlightProbe {
130 socket: ProbeSocket,
131 token: Token,
132 cluster_id: ClusterId,
133 backend: Weak<RefCell<Backend>>,
136 started_at: Instant,
137 timeout: Duration,
138 rise: u32,
139 fall: u32,
140}
141
142#[derive(Default)]
146pub struct UdpHealthChecker {
147 settings: HashMap<ClusterId, UdpHealthSettings>,
150 in_flight: Vec<InFlightProbe>,
151 last_check: HashMap<ClusterId, Instant>,
152 next_token_id: usize,
153 ready_tokens: HashSet<Token>,
154}
155
156impl UdpHealthChecker {
157 pub fn new() -> Self {
158 Self::default()
159 }
160
161 pub fn set_cluster(
169 &mut self,
170 cluster_id: &str,
171 settings: Option<UdpHealthSettings>,
172 registry: &Registry,
173 ) {
174 match settings {
175 Some(s) => {
176 self.settings.insert(cluster_id.into(), s);
177 }
178 None => {
179 self.settings.remove(cluster_id);
180 self.last_check.remove(cluster_id);
181 let mut kept = Vec::with_capacity(self.in_flight.len());
186 for mut probe in self.in_flight.drain(..) {
187 if *probe.cluster_id == *cluster_id {
188 probe.socket.deregister(registry);
189 } else {
190 kept.push(probe);
191 }
192 }
193 self.in_flight = kept;
194 }
195 }
196 }
197
198 pub fn remove_cluster(&mut self, cluster_id: &str, registry: &Registry) {
200 self.set_cluster(cluster_id, None, registry);
201 }
202
203 pub fn owns_token(&self, token: Token) -> bool {
205 token.0 >= UDP_HEALTH_TOKEN_BASE
206 && token.0 < UDP_HEALTH_TOKEN_BASE + UDP_HEALTH_TOKEN_CAPACITY
207 }
208
209 pub fn ready(&mut self, token: Token) {
211 self.ready_tokens.insert(token);
212 }
213
214 fn allocate_token(&mut self) -> Option<Token> {
216 let in_flight: HashSet<usize> = self
217 .in_flight
218 .iter()
219 .map(|p| p.token.0 - UDP_HEALTH_TOKEN_BASE)
220 .collect();
221 for _ in 0..UDP_HEALTH_TOKEN_CAPACITY {
222 let offset = self.next_token_id % UDP_HEALTH_TOKEN_CAPACITY;
223 self.next_token_id = self.next_token_id.wrapping_add(1);
224 if !in_flight.contains(&offset) {
225 let token = Token(UDP_HEALTH_TOKEN_BASE + offset);
226 debug_assert!(
231 self.owns_token(token),
232 "allocate_token returned a token outside the health namespace"
233 );
234 debug_assert!(
235 !self.in_flight.iter().any(|p| p.token == token),
236 "allocate_token returned a token already in flight"
237 );
238 return Some(token);
239 }
240 }
241 error!(
242 "{} token table full ({} in-flight); refusing new probe slot",
243 log_context!(),
244 in_flight.len()
245 );
246 None
247 }
248
249 pub fn poll(&mut self, backends: &Rc<RefCell<BackendMap>>, registry: &Registry) {
253 if self.settings.is_empty() && self.in_flight.is_empty() {
254 return;
255 }
256 self.initiate(backends, registry);
257 self.progress(backends, registry);
258 }
259
260 fn initiate(&mut self, backends: &Rc<RefCell<BackendMap>>, registry: &Registry) {
261 let now = Instant::now();
262 let backend_map = backends.borrow();
263
264 let mut to_probe: ProbeBatch = Vec::new();
266 for (cluster_id, settings) in &self.settings {
267 let due = match self.last_check.get(cluster_id) {
268 Some(last) => now.duration_since(*last) >= settings.interval,
269 None => true,
270 };
271 if !due {
272 continue;
273 }
274 if let Some(list) = backend_map.backends.get(cluster_id) {
275 let targets: Vec<ProbeTarget> = list
279 .backends
280 .iter()
281 .filter(|b| {
282 !self
283 .in_flight
284 .iter()
285 .any(|p| std::ptr::eq(p.backend.as_ptr(), Rc::as_ptr(b)))
286 })
287 .map(|b| (b.borrow().address, Rc::downgrade(b)))
288 .collect();
289 if !targets.is_empty() {
290 to_probe.push((cluster_id.to_owned(), settings.clone(), targets));
291 }
292 }
293 }
294 drop(backend_map);
295
296 for (cluster_id, settings, targets) in to_probe {
297 self.last_check.insert(cluster_id.to_owned(), now);
298 for (address, incarnation) in targets {
299 self.spawn_tcp_probe(
301 backends,
302 registry,
303 &cluster_id,
304 address,
305 &incarnation,
306 &settings,
307 now,
308 );
309 if settings.udp_probe_payload.is_some() {
314 self.spawn_udp_probe(
315 backends,
316 registry,
317 &cluster_id,
318 address,
319 &incarnation,
320 &settings,
321 now,
322 );
323 }
324 }
325 }
326 }
327
328 #[allow(clippy::too_many_arguments)]
332 fn spawn_tcp_probe(
333 &mut self,
334 backends: &Rc<RefCell<BackendMap>>,
335 registry: &Registry,
336 cluster_id: &str,
337 address: SocketAddr,
338 incarnation: &Weak<RefCell<Backend>>,
339 settings: &UdpHealthSettings,
340 now: Instant,
341 ) {
342 let probe_addr = match settings.tcp_port {
343 Some(port) => SocketAddr::new(address.ip(), port),
344 None => address,
345 };
346 let record_failure = || {
347 Self::record(
348 backends,
349 cluster_id,
350 incarnation,
351 false,
352 settings.rise,
353 settings.fall,
354 )
355 };
356 let mut stream = match TcpStream::connect(probe_addr) {
357 Ok(stream) => stream,
358 Err(_) => return record_failure(),
359 };
360 let Some(token) = self.allocate_token() else {
361 return record_failure();
362 };
363 if registry
364 .register(&mut stream, token, Interest::WRITABLE)
365 .is_err()
366 {
367 return record_failure();
368 }
369 self.in_flight.push(InFlightProbe {
370 socket: ProbeSocket::Tcp(stream),
371 token,
372 cluster_id: cluster_id.into(),
373 backend: incarnation.to_owned(),
374 started_at: now,
375 timeout: settings.timeout,
376 rise: settings.rise,
377 fall: settings.fall,
378 });
379 }
380
381 #[allow(clippy::too_many_arguments)]
387 fn spawn_udp_probe(
388 &mut self,
389 backends: &Rc<RefCell<BackendMap>>,
390 registry: &Registry,
391 cluster_id: &str,
392 address: SocketAddr,
393 incarnation: &Weak<RefCell<Backend>>,
394 settings: &UdpHealthSettings,
395 now: Instant,
396 ) {
397 let Some(payload) = settings.udp_probe_payload.as_deref() else {
398 return;
399 };
400 let record_failure = || {
401 Self::record(
402 backends,
403 cluster_id,
404 incarnation,
405 false,
406 settings.rise,
407 settings.fall,
408 )
409 };
410 let mut socket = match udp_connect(address) {
413 Ok(socket) => socket,
414 Err(_) => return record_failure(),
415 };
416 match socket.send(payload) {
420 Ok(_) => {}
421 Err(ref e) if e.kind() == ErrorKind::WouldBlock => return record_failure(),
422 Err(_) => return record_failure(),
423 }
424 let Some(token) = self.allocate_token() else {
425 return record_failure();
426 };
427 if registry
428 .register(&mut socket, token, Interest::READABLE)
429 .is_err()
430 {
431 return record_failure();
432 }
433 self.in_flight.push(InFlightProbe {
434 socket: ProbeSocket::Udp(socket),
435 token,
436 cluster_id: cluster_id.into(),
437 backend: incarnation.to_owned(),
438 started_at: now,
439 timeout: settings.timeout,
440 rise: settings.rise,
441 fall: settings.fall,
442 });
443 }
444
445 fn progress(&mut self, backends: &Rc<RefCell<BackendMap>>, registry: &Registry) {
446 let now = Instant::now();
447 let ready = std::mem::take(&mut self.ready_tokens);
448 let mut completed: Vec<(usize, bool)> = Vec::new();
449
450 for (idx, probe) in self.in_flight.iter_mut().enumerate() {
451 if now.duration_since(probe.started_at) > probe.timeout {
452 completed.push((idx, false));
453 continue;
454 }
455 if !ready.contains(&probe.token) {
456 continue;
457 }
458 let success = match &mut probe.socket {
459 ProbeSocket::Tcp(stream) => {
464 let no_so_error = matches!(stream.take_error(), Ok(None));
465 no_so_error && stream.peer_addr().is_ok()
466 }
467 ProbeSocket::Udp(socket) => {
472 let mut scratch = [0u8; 16];
473 match socket.recv(&mut scratch) {
474 Ok(_) => true,
475 Err(ref e) if e.kind() == ErrorKind::WouldBlock => continue,
476 Err(_) => false,
479 }
480 }
481 };
482 completed.push((idx, success));
483 }
484
485 completed.sort_by_key(|entry| std::cmp::Reverse(entry.0));
486 for (idx, success) in completed {
487 let mut probe = self.in_flight.swap_remove(idx);
488 probe.socket.deregister(registry);
489 Self::record(
490 backends,
491 &probe.cluster_id,
492 &probe.backend,
493 success,
494 probe.rise,
495 probe.fall,
496 );
497 }
498 }
499
500 fn record(
508 backends: &Rc<RefCell<BackendMap>>,
509 cluster_id: &str,
510 incarnation: &Weak<RefCell<Backend>>,
511 success: bool,
512 rise: u32,
513 fall: u32,
514 ) {
515 let mut backend_map = backends.borrow_mut();
516 let Some(list) = backend_map.backends.get_mut(cluster_id) else {
517 return;
518 };
519 let Some(backend_ref) = list.find_incarnation(incarnation) else {
520 debug!(
521 "{} dropping a result for a backend no longer in cluster {}",
522 log_context!(),
523 cluster_id
524 );
525 return;
526 };
527 let mut backend = backend_ref.borrow_mut();
528 let backend_id = backend.backend_id.to_owned();
529 let address = backend.address;
530 if success {
531 if backend.health.record_success(rise) {
532 info!(
533 "{} backend {} at {} marked UP (cluster {})",
534 log_context!(),
535 backend_id,
536 address,
537 cluster_id
538 );
539 incr!(names::udp::BACKEND_HEALTH);
540 }
541 } else if backend.health.record_failure(fall) {
542 warn!(
543 "{} backend {} at {} marked DOWN (cluster {})",
544 log_context!(),
545 backend_id,
546 address,
547 cluster_id
548 );
549 incr!(names::udp::BACKEND_HEALTH);
550 }
551 drop(backend);
552 backend_map.record_cluster_availability(cluster_id);
553 }
554}
555
556#[cfg(test)]
557mod tests {
558 use super::*;
559 use crate::backends::HealthState;
560
561 #[test]
562 fn hysteresis_rise_fall() {
563 let mut state = HealthState::default();
566 assert!(state.is_healthy());
567 assert!(!state.record_failure(3));
568 assert!(!state.record_failure(3));
569 assert!(state.is_healthy());
570 assert!(state.record_failure(3));
571 assert!(!state.is_healthy());
572 assert!(!state.record_success(2));
573 assert!(state.record_success(2));
574 assert!(state.is_healthy());
575 }
576
577 #[test]
578 fn token_namespace_is_disjoint_and_owned() {
579 let hc = UdpHealthChecker::new();
580 assert!(hc.owns_token(Token(UDP_HEALTH_TOKEN_BASE)));
581 assert!(hc.owns_token(Token(UDP_HEALTH_TOKEN_BASE + UDP_HEALTH_TOKEN_CAPACITY - 1)));
582 assert!(!hc.owns_token(Token(UDP_HEALTH_TOKEN_BASE - 1)));
583 assert!(!hc.owns_token(Token(UDP_HEALTH_TOKEN_BASE + UDP_HEALTH_TOKEN_CAPACITY)));
584 assert!(!hc.owns_token(Token(1 << 24)));
586 }
587
588 #[test]
589 fn settings_from_proto_defaults() {
590 let cfg = UdpHealthConfig {
591 mode: None,
592 tcp_port: Some(5353),
593 rise: None,
594 fall: None,
595 fail_open: None,
596 udp_probe_payload: None,
597 probe_interval_seconds: None,
598 probe_timeout_seconds: None,
599 };
600 let s = UdpHealthSettings::from_proto(&cfg);
601 assert_eq!(s.tcp_port, Some(5353));
602 assert_eq!(s.rise, 2);
603 assert_eq!(s.fall, 3);
604 assert_eq!(s.interval, Duration::from_secs(5));
605 assert_eq!(s.timeout, Duration::from_secs(2));
606 }
607
608 #[test]
609 fn udp_probe_payload_is_captured() {
610 let cfg = UdpHealthConfig {
614 mode: Some(sozu_command::proto::command::UdpHealthMode::UdpProbe as i32),
615 tcp_port: None,
616 rise: Some(1),
617 fall: Some(1),
618 fail_open: None,
619 udp_probe_payload: Some(b"PING".to_vec()),
620 probe_interval_seconds: Some(1),
621 probe_timeout_seconds: Some(1),
622 };
623 let s = UdpHealthSettings::from_proto(&cfg);
624 assert_eq!(s.udp_probe_payload.as_deref(), Some(&b"PING"[..]));
625 assert_eq!(s.tcp_port, None);
626 }
627
628 #[test]
634 fn udp_probe_result_feeds_same_hysteresis() {
635 use crate::backends::{Backend, BackendMap};
636
637 let cluster = "dns";
638 let address: SocketAddr = ([127, 0, 0, 1], 5353).into();
639 let backend_map = Rc::new(RefCell::new(BackendMap::new()));
640 backend_map
641 .borrow_mut()
642 .add_backend(cluster, Backend::new("b1", address, None, None, None));
643 let (rise, fall) = (2u32, 3u32);
644 let b1 = Rc::downgrade(&backend_map.borrow().backends[cluster].backends[0]);
645
646 let is_healthy = |map: &Rc<RefCell<BackendMap>>| {
647 let mut m = map.borrow_mut();
648 let list = m.backends.get_mut(cluster).unwrap();
649 let b = list.find_backend(&address).unwrap();
650 b.borrow().health.is_healthy()
651 };
652 assert!(is_healthy(&backend_map));
653
654 for _ in 0..fall {
656 UdpHealthChecker::record(&backend_map, cluster, &b1, false, rise, fall);
657 }
658 assert!(!is_healthy(&backend_map));
659
660 UdpHealthChecker::record(&backend_map, cluster, &b1, true, rise, fall);
663 assert!(!is_healthy(&backend_map));
664 UdpHealthChecker::record(&backend_map, cluster, &b1, true, rise, fall);
665 assert!(is_healthy(&backend_map));
666 }
667
668 fn is_healthy_by_id(map: &Rc<RefCell<BackendMap>>, cluster: &str, backend_id: &str) -> bool {
670 map.borrow().backends[cluster]
671 .backends
672 .iter()
673 .find(|backend| backend.borrow().backend_id == backend_id)
674 .expect("backend id is present")
675 .borrow()
676 .health
677 .is_healthy()
678 }
679
680 #[test]
683 fn probe_result_updates_the_backend_id_that_was_probed() {
684 use crate::backends::{Backend, BackendMap};
685
686 let cluster = "dns";
687 let address: SocketAddr = ([127, 0, 0, 1], 5353).into();
688 let backend_map = Rc::new(RefCell::new(BackendMap::new()));
689 {
690 let mut map = backend_map.borrow_mut();
691 map.add_backend(cluster, Backend::new("a", address, None, None, None));
692 map.add_backend(cluster, Backend::new("b", address, None, None, None));
693 }
694 let probed = Rc::downgrade(
695 backend_map.borrow().backends[cluster]
696 .find_backend_by_identity("b", &address)
697 .expect("backend b is present"),
698 );
699
700 UdpHealthChecker::record(&backend_map, cluster, &probed, false, 1, 1);
701
702 assert_eq!(
703 (
704 is_healthy_by_id(&backend_map, cluster, "a"),
705 is_healthy_by_id(&backend_map, cluster, "b"),
706 ),
707 (true, false),
708 "a failed probe for backend b must mutate b, not the first backend at its address"
709 );
710 }
711
712 #[test]
716 fn late_probe_result_after_remove_and_readd_does_not_mutate_replacement() {
717 use crate::backends::{Backend, BackendMap};
718
719 let cluster = "dns";
720 let address: SocketAddr = ([127, 0, 0, 1], 5354).into();
721 let backend_map = Rc::new(RefCell::new(BackendMap::new()));
722 backend_map
723 .borrow_mut()
724 .add_backend(cluster, Backend::new("b1", address, None, None, None));
725 let held = Rc::clone(&backend_map.borrow().backends[cluster].backends[0]);
728 let stale = Rc::downgrade(&held);
729
730 assert!(
731 backend_map
732 .borrow_mut()
733 .remove_backend(cluster, "b1", &address)
734 );
735 backend_map
736 .borrow_mut()
737 .add_backend(cluster, Backend::new("b1", address, None, None, None));
738
739 UdpHealthChecker::record(&backend_map, cluster, &stale, false, 1, 1);
740 assert!(
741 is_healthy_by_id(&backend_map, cluster, "b1"),
742 "a late result from the removed incarnation must not mark its replacement DOWN"
743 );
744
745 let current = Rc::downgrade(&backend_map.borrow().backends[cluster].backends[0]);
746 UdpHealthChecker::record(&backend_map, cluster, ¤t, false, 1, 1);
747 assert!(
748 !is_healthy_by_id(&backend_map, cluster, "b1"),
749 "a result for the live incarnation must mark it DOWN"
750 );
751 }
752
753 #[test]
758 fn in_flight_probe_for_removed_incarnation_does_not_block_replacement_probe() {
759 use crate::backends::{Backend, BackendMap};
760
761 let cluster = "dns";
762 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
763 let address = listener.local_addr().unwrap();
764 let poll = mio::Poll::new().unwrap();
765 let backend_map = Rc::new(RefCell::new(BackendMap::new()));
766 backend_map
767 .borrow_mut()
768 .add_backend(cluster, Backend::new("b1", address, None, None, None));
769 let settings = UdpHealthSettings::from_proto(&UdpHealthConfig {
770 mode: None,
771 tcp_port: None,
772 rise: Some(1),
773 fall: Some(1),
774 fail_open: None,
775 udp_probe_payload: None,
776 probe_interval_seconds: Some(10),
777 probe_timeout_seconds: Some(5),
778 });
779 let mut checker = UdpHealthChecker::new();
780 checker.set_cluster(cluster, Some(settings), poll.registry());
781
782 checker.initiate(&backend_map, poll.registry());
783 assert_eq!(
784 checker.in_flight.len(),
785 1,
786 "the first cycle probes the backend"
787 );
788
789 let _held = Rc::clone(&backend_map.borrow().backends[cluster].backends[0]);
791 assert!(
792 backend_map
793 .borrow_mut()
794 .remove_backend(cluster, "b1", &address)
795 );
796 backend_map
797 .borrow_mut()
798 .add_backend(cluster, Backend::new("b1", address, None, None, None));
799 let replacement = Rc::clone(&backend_map.borrow().backends[cluster].backends[0]);
800
801 checker.last_check.clear();
804 checker.initiate(&backend_map, poll.registry());
805 assert_eq!(
806 checker.in_flight.len(),
807 2,
808 "the replacement must be probed without waiting for the removed incarnation's probe"
809 );
810 assert!(
811 std::ptr::eq(
812 checker.in_flight[1].backend.as_ptr(),
813 Rc::as_ptr(&replacement)
814 ),
815 "the new probe targets the replacement incarnation"
816 );
817 }
818}