1use deadpool::managed;
9use std::sync::atomic::{AtomicU64, Ordering};
10use thiserror::Error;
11use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
12use tokio::time::timeout;
13
14use crate::constants::pool::{
15 DATE_COMMAND, EXPECTED_DATE_RESPONSE_PREFIX, HEALTH_CHECK_BUFFER_SIZE, HEALTH_CHECK_TIMEOUT,
16 TCP_PEEK_BUFFER_SIZE,
17};
18use crate::stream::ConnectionStream;
19
20#[allow(clippy::cast_precision_loss)] const fn count_as_f64_for_rate(value: u64) -> f64 {
22 value as f64
25}
26
27#[derive(Debug, Error)]
29pub enum HealthCheckError {
30 #[error("TCP connection closed")]
32 TcpClosed,
33
34 #[error("Unexpected data in buffer")]
36 UnexpectedData,
37
38 #[error("TCP error: {0}")]
40 TcpError(std::io::Error),
41
42 #[error("Failed to write health check: {0}")]
44 WriteError(std::io::Error),
45
46 #[error("Failed to read health check response: {0}")]
48 ReadError(std::io::Error),
49
50 #[error("Health check timeout")]
52 Timeout,
53
54 #[error("Unexpected health check response: {0}")]
56 UnexpectedResponse(String),
57
58 #[error("Connection closed during health check")]
60 ConnectionClosedDuringCheck,
61}
62
63impl From<HealthCheckError> for managed::RecycleError<crate::connection_error::ConnectionError> {
64 fn from(err: HealthCheckError) -> Self {
65 Self::Message(err.to_string().into())
66 }
67}
68
69#[derive(Debug, Default)]
71pub struct HealthCheckMetrics {
72 cycles_run: AtomicU64,
74 connections_checked: AtomicU64,
76 connections_failed: AtomicU64,
78}
79
80impl HealthCheckMetrics {
81 #[must_use]
83 pub fn new() -> Self {
84 Self::default()
85 }
86
87 pub fn record_cycle(&self, checked: u64, failed: u64) {
89 self.cycles_run.fetch_add(1, Ordering::Relaxed);
90 self.connections_checked
91 .fetch_add(checked, Ordering::Relaxed);
92 self.connections_failed.fetch_add(failed, Ordering::Relaxed);
93 }
94
95 pub fn failure_rate(&self) -> f64 {
97 let checked = self.connections_checked.load(Ordering::Relaxed);
98 if checked == 0 {
99 0.0
100 } else {
101 let failed = self.connections_failed.load(Ordering::Relaxed);
102 count_as_f64_for_rate(failed) / count_as_f64_for_rate(checked)
103 }
104 }
105
106 pub fn cycles_run(&self) -> u64 {
108 self.cycles_run.load(Ordering::Relaxed)
109 }
110
111 pub fn connections_checked(&self) -> u64 {
113 self.connections_checked.load(Ordering::Relaxed)
114 }
115
116 pub fn connections_failed(&self) -> u64 {
118 self.connections_failed.load(Ordering::Relaxed)
119 }
120}
121
122pub fn check_tcp_alive(
139 conn: &mut ConnectionStream,
140) -> managed::RecycleResult<crate::connection_error::ConnectionError> {
141 if conn.has_pending_bytes() {
142 return Err(HealthCheckError::UnexpectedData.into());
143 }
144
145 let mut peek_buf = [0u8; TCP_PEEK_BUFFER_SIZE];
146
147 let tcp_stream = conn.underlying_tcp_stream();
149 match tcp_stream.try_read(&mut peek_buf) {
150 Ok(0) => return Err(HealthCheckError::TcpClosed.into()),
151 Ok(_) => {
152 return Err(HealthCheckError::UnexpectedData.into());
154 }
155 Err(e) if e.kind() != std::io::ErrorKind::WouldBlock => {
156 return Err(
157 HealthCheckError::TcpError(std::io::Error::new(e.kind(), e.to_string())).into(),
158 );
159 }
160 Err(_) => {}
162 }
163
164 Ok(())
165}
166
167#[inline]
174pub(crate) fn validate_date_response(response: &str) -> Result<(), HealthCheckError> {
175 if response.starts_with(EXPECTED_DATE_RESPONSE_PREFIX) {
176 Ok(())
177 } else {
178 Err(HealthCheckError::UnexpectedResponse(response.to_string()))
179 }
180}
181
182async fn read_date_response<C>(conn: &mut C) -> Result<String, HealthCheckError>
183where
184 C: AsyncRead + Unpin,
185{
186 let mut response_buf = [0u8; HEALTH_CHECK_BUFFER_SIZE];
187 let request = crate::protocol::RequestContext::from_verb_args(b"DATE", b"");
188
189 crate::session::backend::read_single_line_reply(conn, &request, &mut response_buf)
193 .await
194 .map_err(|err| match err {
195 crate::session::backend::SingleLineReplyReadError::Full { bytes_read }
196 | crate::session::backend::SingleLineReplyReadError::Invalid { bytes_read } => {
197 HealthCheckError::UnexpectedResponse(
198 String::from_utf8_lossy(&response_buf[..bytes_read]).into_owned(),
199 )
200 }
201 crate::session::backend::SingleLineReplyReadError::Io(err) => {
202 HealthCheckError::ReadError(err)
203 }
204 crate::session::backend::SingleLineReplyReadError::Closed => {
205 HealthCheckError::ConnectionClosedDuringCheck
206 }
207 })
208}
209
210pub async fn check_date_response<C>(conn: &mut C) -> Result<(), HealthCheckError>
220where
221 C: AsyncRead + AsyncWrite + Unpin,
222{
223 let health_check = async {
225 conn.write_all(DATE_COMMAND)
227 .await
228 .map_err(HealthCheckError::WriteError)?;
229
230 let response = read_date_response(conn).await?;
231 validate_date_response(&response)
232 };
233
234 timeout(HEALTH_CHECK_TIMEOUT, health_check)
236 .await
237 .map_err(|_| HealthCheckError::Timeout)?
238}
239
240#[cfg(test)]
241#[allow(clippy::float_cmp)] mod tests {
243 use super::*;
244 use std::collections::VecDeque;
245 use std::pin::Pin;
246 use std::task::{Context, Poll};
247 use tokio::io::AsyncWrite;
248
249 struct ChunkedStream {
250 chunks: VecDeque<Vec<u8>>,
251 written: Vec<u8>,
252 }
253
254 impl ChunkedStream {
255 fn new(chunks: Vec<Vec<u8>>) -> Self {
256 Self {
257 chunks: chunks.into(),
258 written: Vec::new(),
259 }
260 }
261 }
262
263 impl tokio::io::AsyncRead for ChunkedStream {
264 fn poll_read(
265 mut self: Pin<&mut Self>,
266 _cx: &mut Context<'_>,
267 buf: &mut tokio::io::ReadBuf<'_>,
268 ) -> Poll<std::io::Result<()>> {
269 if let Some(chunk) = self.chunks.pop_front() {
270 let len = chunk.len().min(buf.remaining());
271 buf.put_slice(&chunk[..len]);
272 if len < chunk.len() {
273 self.chunks.push_front(chunk[len..].to_vec());
274 }
275 }
276 Poll::Ready(Ok(()))
277 }
278 }
279
280 impl AsyncWrite for ChunkedStream {
281 fn poll_write(
282 mut self: Pin<&mut Self>,
283 _cx: &mut Context<'_>,
284 buf: &[u8],
285 ) -> Poll<std::io::Result<usize>> {
286 self.written.extend_from_slice(buf);
287 Poll::Ready(Ok(buf.len()))
288 }
289
290 fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
291 Poll::Ready(Ok(()))
292 }
293
294 fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
295 Poll::Ready(Ok(()))
296 }
297 }
298
299 #[test]
300 fn test_health_check_metrics_new() {
301 let metrics = HealthCheckMetrics::new();
302 assert_eq!(metrics.cycles_run(), 0);
303 assert_eq!(metrics.connections_checked(), 0);
304 assert_eq!(metrics.connections_failed(), 0);
305 assert_eq!(metrics.failure_rate(), 0.0);
306 }
307
308 #[test]
309 fn test_health_check_metrics_record_cycle() {
310 let metrics = HealthCheckMetrics::new();
311
312 metrics.record_cycle(10, 2);
313 assert_eq!(metrics.cycles_run(), 1);
314 assert_eq!(metrics.connections_checked(), 10);
315 assert_eq!(metrics.connections_failed(), 2);
316 assert_eq!(metrics.failure_rate(), 0.2);
317
318 metrics.record_cycle(5, 1);
319 assert_eq!(metrics.cycles_run(), 2);
320 assert_eq!(metrics.connections_checked(), 15);
321 assert_eq!(metrics.connections_failed(), 3);
322 assert_eq!(metrics.failure_rate(), 0.2);
323 }
324
325 #[test]
326 fn test_health_check_metrics_failure_rate() {
327 let metrics = HealthCheckMetrics::new();
328
329 metrics.record_cycle(10, 0);
331 assert_eq!(metrics.failure_rate(), 0.0);
332
333 metrics.record_cycle(10, 5);
335 assert!((metrics.failure_rate() - 0.25).abs() < 0.01);
336
337 metrics.record_cycle(10, 10);
339 assert!((metrics.failure_rate() - 0.5).abs() < 0.01);
340 }
341
342 #[test]
343 fn test_health_check_metrics_zero_checked() {
344 let metrics = HealthCheckMetrics::new();
345 assert_eq!(metrics.failure_rate(), 0.0);
346 }
347
348 #[test]
349 fn test_health_check_metrics_multiple_cycles() {
350 let metrics = HealthCheckMetrics::new();
351
352 for i in 1..=5 {
353 metrics.record_cycle(10, 1);
354 assert_eq!(metrics.cycles_run(), i);
355 }
356
357 assert_eq!(metrics.connections_checked(), 50);
358 assert_eq!(metrics.connections_failed(), 5);
359 assert_eq!(metrics.failure_rate(), 0.1);
360 }
361
362 #[tokio::test]
363 async fn test_tcp_alive_check_rejects_queued_backend_bytes() {
364 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
365 let addr = listener.local_addr().unwrap();
366
367 let client_handle =
368 tokio::spawn(async move { tokio::net::TcpStream::connect(addr).await.unwrap() });
369 let (server_stream, _) = listener.accept().await.unwrap();
370 let _client = client_handle.await.unwrap();
371
372 let mut conn = ConnectionStream::plain(server_stream);
373 conn.queue_pending_bytes(b"430 stale response\r\n").unwrap();
374
375 let result = check_tcp_alive(&mut conn);
376 assert!(
377 result.is_err(),
378 "connections with queued backend bytes must not recycle"
379 );
380 }
381
382 #[test]
383 fn test_health_check_error_display() {
384 assert_eq!(
385 HealthCheckError::TcpClosed.to_string(),
386 "TCP connection closed"
387 );
388 assert_eq!(
389 HealthCheckError::UnexpectedData.to_string(),
390 "Unexpected data in buffer"
391 );
392 assert_eq!(
393 HealthCheckError::Timeout.to_string(),
394 "Health check timeout"
395 );
396 assert_eq!(
397 HealthCheckError::ConnectionClosedDuringCheck.to_string(),
398 "Connection closed during health check"
399 );
400 }
401
402 #[test]
403 fn test_health_check_error_unexpected_response() {
404 let err = HealthCheckError::UnexpectedResponse("500 Error".to_string());
405 assert_eq!(
406 err.to_string(),
407 "Unexpected health check response: 500 Error"
408 );
409 }
410
411 #[test]
412 fn test_health_check_error_tcp_error() {
413 let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionReset, "reset");
414 let err = HealthCheckError::TcpError(io_err);
415 assert!(err.to_string().contains("TCP error"));
416 assert!(err.to_string().contains("reset"));
417 }
418
419 #[test]
420 fn test_health_check_error_write_error() {
421 let io_err = std::io::Error::new(std::io::ErrorKind::BrokenPipe, "pipe");
422 let err = HealthCheckError::WriteError(io_err);
423 assert!(err.to_string().contains("Failed to write health check"));
424 }
425
426 #[test]
427 fn test_health_check_error_read_error() {
428 let io_err = std::io::Error::new(std::io::ErrorKind::TimedOut, "timeout");
429 let err = HealthCheckError::ReadError(io_err);
430 assert!(
431 err.to_string()
432 .contains("Failed to read health check response")
433 );
434 }
435
436 #[test]
439 fn test_validate_date_response_success() {
440 assert!(validate_date_response("111 20231215120000\r\n").is_ok());
442 }
443
444 #[test]
445 fn test_validate_date_response_success_minimal() {
446 assert!(validate_date_response("111 \r\n").is_ok());
448 }
449
450 #[test]
451 fn test_validate_date_response_success_with_extra() {
452 assert!(validate_date_response("111 20231215120000 extra info\r\n").is_ok());
454 }
455
456 #[test]
457 fn test_validate_date_response_wrong_code() {
458 let result = validate_date_response("200 OK\r\n");
460 assert!(result.is_err());
461 match result {
462 Err(HealthCheckError::UnexpectedResponse(msg)) => {
463 assert_eq!(msg, "200 OK\r\n");
464 }
465 _ => panic!("Expected UnexpectedResponse error"),
466 }
467 }
468
469 #[test]
470 fn test_validate_date_response_error_code() {
471 assert!(validate_date_response("400 Bad Request\r\n").is_err());
473 assert!(validate_date_response("500 Server Error\r\n").is_err());
474 }
475
476 #[test]
477 fn test_validate_date_response_empty() {
478 let result = validate_date_response("");
480 assert!(result.is_err());
481 }
482
483 #[test]
484 fn test_validate_date_response_malformed() {
485 assert!(validate_date_response("not a valid response").is_err());
487 assert!(validate_date_response("1\r\n").is_err());
488 assert!(validate_date_response("11 \r\n").is_err()); }
490
491 #[test]
492 fn test_validate_date_response_partial_match() {
493 assert!(validate_date_response("110 Info\r\n").is_err());
495 assert!(validate_date_response("112 Other\r\n").is_err());
496 }
497
498 #[test]
499 fn test_validate_date_response_no_space() {
500 assert!(validate_date_response("11120231215120000\r\n").is_err());
502 }
503
504 #[test]
505 fn test_validate_date_response_whitespace_prefix() {
506 assert!(validate_date_response(" 111 20231215120000\r\n").is_err());
508 assert!(validate_date_response("\r\n111 20231215120000\r\n").is_err());
509 }
510
511 #[test]
512 fn test_validate_date_response_case_sensitivity() {
513 assert!(validate_date_response("111 lowercase\r\n").is_ok());
515 assert!(validate_date_response("111 UPPERCASE\r\n").is_ok());
516 }
517
518 #[test]
519 fn test_validate_date_response_unicode() {
520 assert!(validate_date_response("111 日本語\r\n").is_ok());
522 }
523
524 #[test]
525 fn test_validate_date_response_realistic_examples() {
526 assert!(validate_date_response("111 20231215120530\r\n").is_ok());
528 assert!(validate_date_response("111 19700101000000\r\n").is_ok());
529 assert!(validate_date_response("111 20991231235959\r\n").is_ok());
530 }
531
532 #[tokio::test]
533 async fn test_check_date_response_reads_split_reply() {
534 let mut stream = ChunkedStream::new(vec![b"111 20231215".to_vec(), b"120000\r\n".to_vec()]);
535
536 let result = check_date_response(&mut stream).await;
537 assert!(
538 result.is_ok(),
539 "split DATE responses should be consumed fully"
540 );
541 assert_eq!(stream.written, DATE_COMMAND);
542 }
543
544 #[tokio::test]
545 async fn test_check_date_response_rejects_invalid_reply_bytes() {
546 let mut stream = ChunkedStream::new(vec![b"abc\r\n".to_vec()]);
547
548 let result = check_date_response(&mut stream).await;
549
550 match result {
551 Err(HealthCheckError::UnexpectedResponse(response)) => {
552 assert_eq!(response, "abc\r\n");
553 }
554 other => panic!("Expected invalid DATE response, got {other:?}"),
555 }
556 }
557}