use crate::pool::Pool;
use ecat_data::{BackendKind, TRANSACTIONS_LEAKED, timeout_counter};
use std::sync::atomic::{AtomicU64, Ordering};
static POOL_TIMEOUTS: AtomicU64 = AtomicU64::new(0);
pub(crate) fn count_pool_timeout() {
POOL_TIMEOUTS.fetch_add(1, Ordering::Relaxed);
}
pub fn register_pool_metrics(backend: &'static str, pool: &Pool) {
let pool = pool.clone();
ecat_metrics::register_rdbms_metrics(
backend,
Box::new(move || {
let idle = u64::from(pool.idle());
let size = u64::from(pool.size());
(idle, size.saturating_sub(idle))
}),
Box::new(|| POOL_TIMEOUTS.load(Ordering::Relaxed)),
Box::new(|| timeout_counter(BackendKind::Rdbms).load(Ordering::Relaxed)),
Box::new(|| TRANSACTIONS_LEAKED.load(Ordering::Relaxed)),
);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::SqlxClient;
use ecat_data::RdbmsError;
async fn client() -> SqlxClient {
SqlxClient::connect("sqlite::memory:").await.unwrap()
}
fn sample(text: &str, prefix: &str) -> Option<f64> {
text.lines()
.find(|l| l.starts_with(prefix) && !l.starts_with('#'))
.and_then(|l| l.rsplit(' ').next())
.and_then(|v| v.parse().ok())
}
fn assert_bracketed(text: &str, prefix: &str, lo: u64, hi: u64) {
let v =
sample(text, prefix).unwrap_or_else(|| panic!("缺指标 {prefix},实际输出:\n{text}"));
assert!(
v >= lo as f64 && v <= hi as f64,
"{prefix} 应落在 [{lo}, {hi}],实际 {v} —— 必须直读进程级静态量"
);
}
#[tokio::test]
async fn registers_all_four_metrics_for_the_backend() {
let c = client().await;
register_pool_metrics("sqlx-sqlite-test", c.pool());
let q_lo = timeout_counter(BackendKind::Rdbms).fetch_add(1, Ordering::Relaxed) + 1;
let l_lo = TRANSACTIONS_LEAKED.fetch_add(1, Ordering::Relaxed) + 1;
let text = ecat_metrics::metrics_text();
let q_hi = timeout_counter(BackendKind::Rdbms).load(Ordering::Relaxed);
let l_hi = TRANSACTIONS_LEAKED.load(Ordering::Relaxed);
for name in [
"ecat_rdbms_pool_connections",
"ecat_rdbms_pool_timeouts_total",
"ecat_rdbms_query_timeout_total",
"ecat_rdbms_transactions_leaked_total",
] {
assert!(
text.contains(&format!("{name}{{backend=\"sqlx-sqlite-test\"")),
"缺指标 {name},实际输出:\n{text}"
);
}
assert_bracketed(
&text,
"ecat_rdbms_query_timeout_total{backend=\"sqlx-sqlite-test\"}",
q_lo,
q_hi,
);
assert_bracketed(
&text,
"ecat_rdbms_transactions_leaked_total{backend=\"sqlx-sqlite-test\"}",
l_lo,
l_hi,
);
}
#[tokio::test]
async fn gauge_reflects_live_pool_state() {
let c = client().await;
register_pool_metrics("sqlx-live-test", c.pool());
let idle = || {
sample(
&ecat_metrics::metrics_text(),
"ecat_rdbms_pool_connections{backend=\"sqlx-live-test\",state=\"idle\"}",
)
};
let active = || {
sample(
&ecat_metrics::metrics_text(),
"ecat_rdbms_pool_connections{backend=\"sqlx-live-test\",state=\"active\"}",
)
};
assert_eq!(idle(), Some(1.0), "刚建好池应当有一条空闲连接");
assert_eq!(active(), Some(0.0));
let guard = c.pool().acquire().await.unwrap();
assert_eq!(idle(), Some(0.0), "连接被借出后不应再算空闲");
assert_eq!(active(), Some(1.0));
drop(guard);
}
#[tokio::test]
async fn pool_timeout_is_counted_by_db_err() {
let before = POOL_TIMEOUTS.load(Ordering::Relaxed);
let err = crate::db_err(sqlx::Error::PoolTimedOut);
assert!(matches!(err, RdbmsError::Database(_)), "got: {err:?}");
let counted = POOL_TIMEOUTS.load(Ordering::Relaxed);
assert_eq!(counted, before + 1);
let err = crate::db_err(sqlx::Error::RowNotFound);
assert!(matches!(err, RdbmsError::Database(_)));
assert_eq!(
POOL_TIMEOUTS.load(Ordering::Relaxed),
counted,
"非 PoolTimedOut 不该计数"
);
let c = client().await;
register_pool_metrics("sqlx-timeout-test", c.pool());
let text = ecat_metrics::metrics_text();
let hi = POOL_TIMEOUTS.load(Ordering::Relaxed);
assert_bracketed(
&text,
"ecat_rdbms_pool_timeouts_total{backend=\"sqlx-timeout-test\"}",
counted,
hi,
);
}
#[tokio::test]
async fn same_backend_registration_replaces_instead_of_duplicating() {
let a = client().await;
let b = client().await;
register_pool_metrics("sqlx-dup-test", a.pool());
register_pool_metrics("sqlx-dup-test", b.pool());
let text = ecat_metrics::metrics_text();
let hits = text
.lines()
.filter(|l| {
l.starts_with(
"ecat_rdbms_pool_connections{backend=\"sqlx-dup-test\",state=\"idle\"}",
)
})
.count();
assert_eq!(hits, 1, "重复注册应覆盖而不是追加,实际输出:\n{text}");
}
}