1use crate::Backend;
18use arc_swap::ArcSwap;
19use async_trait::async_trait;
20use pingora_core::connectors::http::custom;
21use pingora_core::connectors::{http::Connector as HttpConnector, TransportConnector};
22use pingora_core::custom_session;
23use pingora_core::protocols::http::custom::client::Session;
24use pingora_core::upstreams::peer::{BasicPeer, HttpPeer, Peer};
25use pingora_error::{Error, ErrorType::CustomCode, Result};
26use pingora_http::{RequestHeader, ResponseHeader};
27use std::sync::Arc;
28use std::time::Duration;
29
30#[async_trait]
33pub trait HealthObserve {
34 async fn observe(&self, target: &Backend, healthy: bool);
36}
37pub type HealthObserveCallback = Box<dyn HealthObserve + Send + Sync>;
39
40pub type BackendSummary = Box<dyn Fn(&Backend) -> String + Send + Sync>;
42
43#[async_trait]
45pub trait HealthCheck {
46 async fn check(&self, target: &Backend) -> Result<()>;
53
54 async fn health_status_change(&self, _target: &Backend, _healthy: bool) {}
56
57 fn backend_summary(&self, target: &Backend) -> String {
59 format!("{target:?}")
60 }
61
62 fn health_threshold(&self, success: bool) -> usize;
67}
68
69pub struct TcpHealthCheck {
73 pub consecutive_success: usize,
75 pub consecutive_failure: usize,
77 pub peer_template: BasicPeer,
86 connector: TransportConnector,
87 pub health_changed_callback: Option<HealthObserveCallback>,
89}
90
91impl Default for TcpHealthCheck {
92 fn default() -> Self {
93 let mut peer_template = BasicPeer::new("0.0.0.0:1");
94 peer_template.options.connection_timeout = Some(Duration::from_secs(1));
95 TcpHealthCheck {
96 consecutive_success: 1,
97 consecutive_failure: 1,
98 peer_template,
99 connector: TransportConnector::new(None),
100 health_changed_callback: None,
101 }
102 }
103}
104
105impl TcpHealthCheck {
106 pub fn new() -> Box<Self> {
111 Box::<TcpHealthCheck>::default()
112 }
113
114 pub fn new_tls(sni: &str) -> Box<Self> {
118 let mut new = Self::default();
119 new.peer_template.sni = sni.into();
120 Box::new(new)
121 }
122
123 pub fn set_connector(&mut self, connector: TransportConnector) {
125 self.connector = connector;
126 }
127}
128
129#[async_trait]
130impl HealthCheck for TcpHealthCheck {
131 fn health_threshold(&self, success: bool) -> usize {
132 if success {
133 self.consecutive_success
134 } else {
135 self.consecutive_failure
136 }
137 }
138
139 async fn check(&self, target: &Backend) -> Result<()> {
140 let mut peer = self.peer_template.clone();
141 peer._address = target.addr.clone();
142 self.connector.get_stream(&peer).await.map(|_| {})
143 }
144
145 async fn health_status_change(&self, target: &Backend, healthy: bool) {
146 if let Some(callback) = &self.health_changed_callback {
147 callback.observe(target, healthy).await;
148 }
149 }
150}
151
152type Validator = Box<dyn Fn(&ResponseHeader) -> Result<()> + Send + Sync>;
153
154pub struct HttpHealthCheck<C = ()>
158where
159 C: custom::Connector,
160{
161 pub consecutive_success: usize,
163 pub consecutive_failure: usize,
165 pub peer_template: HttpPeer,
173 pub reuse_connection: bool,
180 pub req: RequestHeader,
182 connector: HttpConnector<C>,
183 pub validator: Option<Validator>,
187 pub port_override: Option<u16>,
190 pub health_changed_callback: Option<HealthObserveCallback>,
192 pub backend_summary_callback: Option<BackendSummary>,
194}
195
196impl HttpHealthCheck<()> {
197 pub fn new(host: &str, tls: bool) -> Self {
206 let mut req = RequestHeader::build("GET", b"/", None).unwrap();
207 req.append_header("Host", host).unwrap();
208 let sni = if tls { host.into() } else { String::new() };
209 let mut peer_template = HttpPeer::new("0.0.0.0:1", tls, sni);
210 peer_template.options.connection_timeout = Some(Duration::from_secs(1));
211 peer_template.options.read_timeout = Some(Duration::from_secs(1));
212 HttpHealthCheck {
213 consecutive_success: 1,
214 consecutive_failure: 1,
215 peer_template,
216 connector: HttpConnector::new(None),
217 reuse_connection: false,
218 req,
219 validator: None,
220 port_override: None,
221 health_changed_callback: None,
222 backend_summary_callback: None,
223 }
224 }
225}
226
227impl<C> HttpHealthCheck<C>
228where
229 C: custom::Connector,
230{
231 pub fn new_custom(host: &str, tls: bool, custom: HttpConnector<C>) -> Self {
240 let mut req = RequestHeader::build("GET", b"/", None).unwrap();
241 req.append_header("Host", host).unwrap();
242 let sni = if tls { host.into() } else { String::new() };
243 let mut peer_template = HttpPeer::new("0.0.0.0:1", tls, sni);
244 peer_template.options.connection_timeout = Some(Duration::from_secs(1));
245 peer_template.options.read_timeout = Some(Duration::from_secs(1));
246 HttpHealthCheck {
247 consecutive_success: 1,
248 consecutive_failure: 1,
249 peer_template,
250 connector: custom,
251 reuse_connection: false,
252 req,
253 validator: None,
254 port_override: None,
255 health_changed_callback: None,
256 backend_summary_callback: None,
257 }
258 }
259
260 pub fn set_connector(&mut self, connector: HttpConnector<C>) {
262 self.connector = connector;
263 }
264
265 pub fn set_backend_summary<F>(&mut self, callback: F)
266 where
267 F: Fn(&Backend) -> String + Send + Sync + 'static,
268 {
269 self.backend_summary_callback = Some(Box::new(callback));
270 }
271}
272
273#[async_trait]
274impl<C> HealthCheck for HttpHealthCheck<C>
275where
276 C: custom::Connector,
277{
278 fn health_threshold(&self, success: bool) -> usize {
279 if success {
280 self.consecutive_success
281 } else {
282 self.consecutive_failure
283 }
284 }
285
286 async fn check(&self, target: &Backend) -> Result<()> {
287 let mut peer = self.peer_template.clone();
288 peer._address = target.addr.clone();
289 if let Some(port) = self.port_override {
290 peer._address.set_port(port);
291 }
292 let session = self.connector.get_http_session(&peer).await?;
293
294 let mut session = session.0;
295
296 session.set_write_timeout(peer.options.write_timeout);
297
298 let req = Box::new(self.req.clone());
299 session.write_request_header(req).await?;
300 session.finish_request_body().await?;
301
302 custom_session!(session.finish_custom().await?);
303
304 session.set_read_timeout(peer.options.read_timeout);
305
306 session.read_response_header().await?;
307
308 let resp = session.response_header().expect("just read");
309
310 if let Some(validator) = self.validator.as_ref() {
311 validator(resp)?;
312 } else if resp.status != 200 {
313 return Error::e_explain(
314 CustomCode("non 200 code", resp.status.as_u16()),
315 "during http healthcheck",
316 );
317 };
318
319 while session.read_response_body().await?.is_some() {
320 }
322
323 custom_session!(session.drain_custom_messages().await?);
325
326 if self.reuse_connection {
327 let idle_timeout = peer.idle_timeout();
328 self.connector
329 .release_http_session(session, &peer, idle_timeout)
330 .await;
331 }
332
333 Ok(())
334 }
335 async fn health_status_change(&self, target: &Backend, healthy: bool) {
336 if let Some(callback) = &self.health_changed_callback {
337 callback.observe(target, healthy).await;
338 }
339 }
340 fn backend_summary(&self, target: &Backend) -> String {
341 if let Some(callback) = &self.backend_summary_callback {
342 callback(target)
343 } else {
344 format!("{target:?}")
345 }
346 }
347}
348
349#[derive(Clone)]
350struct HealthInner {
351 healthy: bool,
353 consecutive_counter: usize,
357}
358
359#[derive(Clone)]
364pub(crate) struct Health(Arc<ArcSwap<HealthInner>>);
365
366impl Default for Health {
367 fn default() -> Self {
368 Health(Arc::new(ArcSwap::new(Arc::new(HealthInner {
369 healthy: true, consecutive_counter: 0,
371 }))))
372 }
373}
374
375impl Health {
376 pub fn ready(&self) -> bool {
377 self.0.load().healthy
378 }
379
380 pub fn observe_health(&self, health: bool, flip_threshold: usize) -> bool {
382 let h = self.0.load();
383 let mut flipped = false;
384 if h.healthy != health {
385 let mut new_health = (**h).clone();
388 new_health.consecutive_counter += 1;
389 if new_health.consecutive_counter >= flip_threshold {
390 new_health.healthy = health;
391 new_health.consecutive_counter = 0;
392 flipped = true;
393 }
394 self.0.store(Arc::new(new_health));
395 } else if h.consecutive_counter > 0 {
396 let mut new_health = (**h).clone();
399 new_health.consecutive_counter = 0;
400 self.0.store(Arc::new(new_health));
401 }
402 flipped
403 }
404}
405
406#[cfg(test)]
407mod test {
408 use std::{
409 collections::{BTreeSet, HashMap},
410 sync::atomic::{AtomicU16, Ordering},
411 };
412
413 use super::*;
414 use crate::{discovery, Backends, SocketAddr};
415 use async_trait::async_trait;
416 use http::Extensions;
417
418 #[tokio::test]
419 async fn test_tcp_check() {
420 let tcp_check = TcpHealthCheck::default();
421
422 let backend = Backend {
423 addr: SocketAddr::Inet("1.1.1.1:80".parse().unwrap()),
424 weight: 1,
425 ext: Extensions::new(),
426 };
427
428 assert!(tcp_check.check(&backend).await.is_ok());
429
430 let backend = Backend {
431 addr: SocketAddr::Inet("1.1.1.1:79".parse().unwrap()),
432 weight: 1,
433 ext: Extensions::new(),
434 };
435
436 assert!(tcp_check.check(&backend).await.is_err());
437 }
438
439 #[cfg(feature = "any_tls")]
440 #[tokio::test]
441 async fn test_tls_check() {
442 let tls_check = TcpHealthCheck::new_tls("one.one.one.one");
443 let backend = Backend {
444 addr: SocketAddr::Inet("1.1.1.1:443".parse().unwrap()),
445 weight: 1,
446 ext: Extensions::new(),
447 };
448
449 assert!(tls_check.check(&backend).await.is_ok());
450 }
451
452 #[cfg(feature = "any_tls")]
453 #[tokio::test]
454 async fn test_https_check() {
455 let https_check = HttpHealthCheck::new("one.one.one.one", true);
456
457 let backend = Backend {
458 addr: SocketAddr::Inet("1.1.1.1:443".parse().unwrap()),
459 weight: 1,
460 ext: Extensions::new(),
461 };
462
463 assert!(https_check.check(&backend).await.is_ok());
464 }
465
466 #[tokio::test]
467 async fn test_http_custom_check() {
468 let mut http_check = HttpHealthCheck::new("one.one.one.one", false);
469 http_check.validator = Some(Box::new(|resp: &ResponseHeader| {
470 if resp.status == 301 {
471 Ok(())
472 } else {
473 Error::e_explain(
474 CustomCode("non 301 code", resp.status.as_u16()),
475 "during http healthcheck",
476 )
477 }
478 }));
479
480 let backend = Backend {
481 addr: SocketAddr::Inet("1.1.1.1:80".parse().unwrap()),
482 weight: 1,
483 ext: Extensions::new(),
484 };
485
486 http_check.check(&backend).await.unwrap();
487
488 assert!(http_check.check(&backend).await.is_ok());
489 }
490
491 #[tokio::test]
492 async fn test_health_observe() {
493 struct Observe {
494 unhealthy_count: Arc<AtomicU16>,
495 }
496 #[async_trait]
497 impl HealthObserve for Observe {
498 async fn observe(&self, _target: &Backend, healthy: bool) {
499 if !healthy {
500 self.unhealthy_count.fetch_add(1, Ordering::Relaxed);
501 }
502 }
503 }
504
505 let good_backend = Backend::new("127.0.0.1:79").unwrap();
506 let new_good_backends = || -> (BTreeSet<Backend>, HashMap<u64, bool>) {
507 let mut healthy = HashMap::new();
508 healthy.insert(good_backend.hash_key(), true);
509 let mut backends = BTreeSet::new();
510 backends.extend(vec![good_backend.clone()]);
511 (backends, healthy)
512 };
513 {
515 let unhealthy_count = Arc::new(AtomicU16::new(0));
516 let ob = Observe {
517 unhealthy_count: unhealthy_count.clone(),
518 };
519 let bob = Box::new(ob);
520 let tcp_check = TcpHealthCheck {
521 health_changed_callback: Some(bob),
522 ..Default::default()
523 };
524
525 let discovery = discovery::Static::default();
526 let mut backends = Backends::new(Box::new(discovery));
527 backends.set_health_check(Box::new(tcp_check));
528 let result = new_good_backends();
529 backends.do_update(result.0, result.1, |_backend: Arc<BTreeSet<Backend>>| {});
530 assert!(backends.ready(&good_backend));
532
533 backends.run_health_check(false).await;
535 assert!(1 == unhealthy_count.load(Ordering::Relaxed));
536 assert!(!backends.ready(&good_backend));
538 }
539
540 {
542 let unhealthy_count = Arc::new(AtomicU16::new(0));
543 let ob = Observe {
544 unhealthy_count: unhealthy_count.clone(),
545 };
546 let bob = Box::new(ob);
547
548 let mut https_check = HttpHealthCheck::new("one.one.one.one", true);
549 https_check.health_changed_callback = Some(bob);
550
551 let discovery = discovery::Static::default();
552 let mut backends = Backends::new(Box::new(discovery));
553 backends.set_health_check(Box::new(https_check));
554 let result = new_good_backends();
555 backends.do_update(result.0, result.1, |_backend: Arc<BTreeSet<Backend>>| {});
556 assert!(backends.ready(&good_backend));
558 backends.run_health_check(false).await;
560 assert!(1 == unhealthy_count.load(Ordering::Relaxed));
561 assert!(!backends.ready(&good_backend));
562 }
563 }
564}