#[cfg(feature = "permission")]
use crate::access::RolePolicy;
use crate::i18n;
#[cfg(feature = "permission")]
use arc_swap::ArcSwapOption;
use async_trait::async_trait;
#[cfg(feature = "permission")]
use oxcache::Cache;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use std::time::Duration;
#[cfg(feature = "metrics")]
use std::time::Instant;
use tokio::sync::{Mutex as AsyncMutex, Notify, Semaphore};
#[cfg(feature = "pool-health-check")]
use tokio::time::interval;
use tokio::time::timeout;
use super::Session;
#[cfg(feature = "permission")]
use crate::access::PermissionConfig;
#[cfg(any(feature = "ladybug", feature = "neo4j"))]
use crate::database::GraphConnection;
use crate::foundation::{ConfigError, DbConfig};
use crate::foundation::{DbError, DbResult};
#[cfg(feature = "metrics")]
use crate::observability::MetricsCollector;
use sea_orm::ConnectionTrait;
pub type DatabaseConnection = sea_orm::DatabaseConnection;
#[derive(Clone)]
pub enum DbConnection {
SeaOrm(DatabaseConnection),
#[cfg(feature = "duckdb")]
DuckDb(crate::database::DuckDbConnection),
#[cfg(feature = "ladybug")]
Ladybug(Arc<crate::database::LadybugConnection>),
#[cfg(feature = "neo4j")]
Neo4j(Arc<crate::database::Neo4jConnection>),
}
impl DbConnection {
pub fn as_sea_orm(&self) -> DbResult<&DatabaseConnection> {
match self {
DbConnection::SeaOrm(conn) => Ok(conn),
#[cfg(feature = "duckdb")]
DbConnection::DuckDb(_) => Err(DbError::Connection(sea_orm::DbErr::Custom(
"Operation requires SeaORM connection but got DuckDb".to_string(),
))),
#[cfg(feature = "ladybug")]
DbConnection::Ladybug(_) => Err(DbError::Connection(sea_orm::DbErr::Custom(
"Operation requires SeaORM connection but got Ladybug".to_string(),
))),
#[cfg(feature = "neo4j")]
DbConnection::Neo4j(_) => Err(DbError::Connection(sea_orm::DbErr::Custom(
"Operation requires SeaORM connection but got Neo4j".to_string(),
))),
}
}
#[cfg(feature = "duckdb")]
pub fn as_duckdb(&self) -> DbResult<&crate::database::DuckDbConnection> {
match self {
DbConnection::DuckDb(conn) => Ok(conn),
DbConnection::SeaOrm(_) => Err(DbError::Connection(sea_orm::DbErr::Custom(
"Operation requires DuckDb connection but got SeaOrm".to_string(),
))),
#[cfg(feature = "ladybug")]
DbConnection::Ladybug(_) => Err(DbError::Connection(sea_orm::DbErr::Custom(
"Operation requires DuckDb connection but got Ladybug".to_string(),
))),
#[cfg(feature = "neo4j")]
DbConnection::Neo4j(_) => Err(DbError::Connection(sea_orm::DbErr::Custom(
"Operation requires DuckDb connection but got Neo4j".to_string(),
))),
}
}
#[cfg(any(feature = "ladybug", feature = "neo4j"))]
pub fn as_graph(&self) -> DbResult<&dyn crate::database::GraphConnection> {
match self {
#[cfg(feature = "ladybug")]
DbConnection::Ladybug(conn) => Ok(conn.as_ref()),
#[cfg(feature = "neo4j")]
DbConnection::Neo4j(conn) => Ok(conn.as_ref()),
DbConnection::SeaOrm(_) => Err(DbError::Connection(sea_orm::DbErr::Custom(
"Operation requires graph connection but got SeaOrm".to_string(),
))),
#[cfg(feature = "duckdb")]
DbConnection::DuckDb(_) => Err(DbError::Connection(sea_orm::DbErr::Custom(
"Operation requires graph connection but got DuckDb".to_string(),
))),
}
}
pub fn is_duckdb(&self) -> bool {
#[cfg(feature = "duckdb")]
{
matches!(self, DbConnection::DuckDb(_))
}
#[cfg(not(feature = "duckdb"))]
{
false
}
}
pub fn is_graph(&self) -> bool {
#[cfg(feature = "ladybug")]
{
if matches!(self, DbConnection::Ladybug(_)) {
return true;
}
}
#[cfg(feature = "neo4j")]
{
if matches!(self, DbConnection::Neo4j(_)) {
return true;
}
}
false
}
}
impl std::fmt::Debug for DbConnection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DbConnection::SeaOrm(_) => write!(f, "DbConnection::SeaOrm(..)"),
#[cfg(feature = "duckdb")]
DbConnection::DuckDb(conn) => write!(f, "DbConnection::DuckDb({conn:?})"),
#[cfg(feature = "ladybug")]
DbConnection::Ladybug(conn) => write!(f, "DbConnection::Ladybug({conn:?})"),
#[cfg(feature = "neo4j")]
DbConnection::Neo4j(conn) => write!(f, "DbConnection::Neo4j({conn:?})"),
}
}
}
#[derive(Clone)]
pub struct DbPool {
inner: Arc<DbPoolInner>,
#[cfg(any(feature = "cache", feature = "oxcache-integration"))]
cache_provider: Option<Arc<dyn crate::domain::DbCacheProvider + Send + Sync>>,
}
pub(crate) struct DbPoolInner {
pub(crate) config: DbConfig,
connection_semaphore: Arc<Semaphore>,
idle_connections: AsyncMutex<Vec<DbConnection>>,
connection_available: Notify,
pub(super) active_count: AtomicU32,
pub(super) total_count: AtomicU32,
#[cfg(feature = "permission")]
pub(crate) policy_cache: Arc<Cache<String, RolePolicy>>,
#[cfg(feature = "permission")]
permission_config: Arc<ArcSwapOption<PermissionConfig>>,
health_check_shutdown: Arc<Notify>,
pub(super) admin_role: String,
#[cfg(feature = "metrics")]
pub(crate) metrics_collector: Option<Arc<MetricsCollector>>,
pub(super) wait_count: AtomicU32,
pub(super) max_waiters: AtomicU32,
pub(super) borrow_count: AtomicU64,
pub(super) max_active: AtomicU32,
#[cfg(feature = "failover")]
#[allow(dead_code)]
pub(super) current_url_index: std::sync::atomic::AtomicU32,
}
impl DbPoolInner {
pub(crate) fn release_connection(inner: &Arc<Self>, conn: DbConnection) {
inner.active_count.fetch_sub(1, Ordering::SeqCst);
let inner_clone = Arc::clone(inner);
if let Ok(mut idle) = inner_clone.idle_connections.try_lock() {
if idle.len() < inner_clone.config.pool_config.max_connections as usize {
idle.push(conn);
inner_clone.connection_available.notify_one();
inner_clone.connection_semaphore.add_permits(1);
} else {
inner_clone.total_count.fetch_sub(1, Ordering::SeqCst);
inner_clone.connection_semaphore.add_permits(1);
}
return;
}
if tokio::runtime::Handle::try_current().is_ok() {
tokio::spawn(async move {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
}));
if result.is_err() {
inner_clone.total_count.fetch_sub(1, Ordering::SeqCst);
inner_clone.connection_semaphore.add_permits(1);
return;
}
let mut idle = inner_clone.idle_connections.lock().await;
if idle.len() < inner_clone.config.pool_config.max_connections as usize {
idle.push(conn);
inner_clone.connection_available.notify_one();
} else {
inner_clone.total_count.fetch_sub(1, Ordering::SeqCst);
}
inner_clone.connection_semaphore.add_permits(1);
});
} else {
inner_clone.total_count.fetch_sub(1, Ordering::SeqCst);
inner_clone.connection_semaphore.add_permits(1);
}
}
}
impl DbPool {
fn update_max_active(&self, active: u32) {
let mut current = self.inner.max_active.load(Ordering::Acquire);
while active > current {
match self
.inner
.max_active
.compare_exchange(current, active, Ordering::SeqCst, Ordering::Acquire)
{
Ok(_) => return,
Err(observed) => {
current = observed;
}
}
}
}
#[cfg(any(feature = "cache", feature = "oxcache-integration"))]
pub fn set_cache_provider(&mut self, provider: Arc<dyn crate::domain::DbCacheProvider + Send + Sync>) {
self.cache_provider = Some(provider);
}
#[cfg(any(feature = "cache", feature = "oxcache-integration"))]
pub fn cache_provider(&self) -> Option<&Arc<dyn crate::domain::DbCacheProvider + Send + Sync>> {
self.cache_provider.as_ref()
}
pub async fn new(url: &str) -> DbResult<Self> {
let config = DbConfig {
url: url.to_string(),
..Default::default()
};
Self::with_config(config).await
}
pub async fn with_config(config: DbConfig) -> DbResult<Self> {
config
.validate()
.map_err(|e| DbError::Config(i18n::t("pool-invalid-config", &[("error", e.to_string())])))?;
let _connection = Self::create_connection(&config).await?;
#[cfg(feature = "permission")]
let (policy_cache, permission_config) = Self::setup_permission_cache(&config).await?;
let pool = Self {
inner: Arc::new(DbPoolInner {
config: config.clone(),
connection_semaphore: Arc::new(Semaphore::new(config.pool_config.max_connections as usize)),
idle_connections: AsyncMutex::new(Vec::new()),
connection_available: Notify::new(),
active_count: AtomicU32::new(0),
total_count: AtomicU32::new(0),
#[cfg(feature = "permission")]
policy_cache,
#[cfg(feature = "permission")]
permission_config: match permission_config {
Some(config) => Arc::new(ArcSwapOption::from_pointee(config)),
None => Arc::new(ArcSwapOption::empty()),
},
health_check_shutdown: Arc::new(Notify::new()),
admin_role: config.admin_role.clone(),
#[cfg(feature = "metrics")]
metrics_collector: None,
wait_count: AtomicU32::new(0),
max_waiters: AtomicU32::new(0),
borrow_count: AtomicU64::new(0),
max_active: AtomicU32::new(0),
#[cfg(feature = "failover")]
current_url_index: std::sync::atomic::AtomicU32::new(0),
}),
#[cfg(any(feature = "cache", feature = "oxcache-integration"))]
cache_provider: None,
};
super::audit::warn_if_default_admin_role_used(&config.admin_role);
#[cfg(feature = "pool-health-check")]
pool.start_background_health_check();
#[cfg(feature = "pool-warmup")]
pool.warmup_connections().await?;
#[cfg(feature = "auto-migrate")]
if config.auto_migrate
&& let Some(ref migrations_dir) = config.migrations_dir
{
if migrations_dir.exists() {
let _applied = pool.run_migrations(migrations_dir).await?;
} else {
}
}
Ok(pool)
}
#[cfg_attr(
feature = "sqlite",
doc = r###"
/// ```rust
/// use dbnexus::DbPool;
/// use dbnexus::DbConfig;
///
/// #[tokio::main]
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let config = DbConfig {
/// url: "sqlite::memory:".to_string(),
/// pool_config: dbnexus::foundation::PoolConfig {
/// max_connections: 10,
/// min_connections: 2,
/// ..Default::default()
/// },
/// ..Default::default()
/// };
///
/// let pool = DbPool::try_from_config(config).await?;
/// Ok(())
/// }
/// ```
"###
)]
#[cfg_attr(
not(feature = "sqlite"),
doc = r###"
/// ```rust,ignore
/// // 此文档测试需要 sqlite 特性
/// // 在使用其他数据库时,请参考相应的文档和示例
/// ```
"###
)]
pub async fn try_from_config(config: DbConfig) -> DbResult<Self> {
Self::with_config(config).await
}
#[cfg(not(feature = "permission"))]
pub fn try_from(config: &DbConfig) -> Result<Self, ConfigError> {
super::audit::warn_if_default_admin_role_used(&config.admin_role);
Ok(Self {
inner: Arc::new(DbPoolInner {
config: config.clone(),
connection_semaphore: Arc::new(Semaphore::new(config.pool_config.max_connections as usize)),
idle_connections: AsyncMutex::new(Vec::new()),
connection_available: Notify::new(),
active_count: AtomicU32::new(0),
total_count: AtomicU32::new(0),
health_check_shutdown: Arc::new(Notify::new()),
admin_role: config.admin_role.clone(),
#[cfg(feature = "metrics")]
metrics_collector: None,
wait_count: AtomicU32::new(0),
max_waiters: AtomicU32::new(0),
borrow_count: AtomicU64::new(0),
max_active: AtomicU32::new(0),
#[cfg(feature = "failover")]
current_url_index: std::sync::atomic::AtomicU32::new(0),
}),
#[cfg(any(feature = "cache", feature = "oxcache-integration"))]
cache_provider: None,
})
}
#[cfg(feature = "permission")]
pub fn try_from(config: &DbConfig) -> Result<Self, ConfigError> {
let _ = config;
Err(ConfigError::InvalidValue {
key: "permission".to_string(),
message: "DbPool::try_from cannot be used with `permission` feature enabled; \
use `DbPool::with_config(config).await` instead (async constructor required for cache initialization)"
.to_string(),
})
}
#[cfg(feature = "permission")]
async fn setup_permission_cache(
config: &DbConfig,
) -> DbResult<(Arc<Cache<String, RolePolicy>>, Option<PermissionConfig>)> {
let policy_cache = Arc::new(
Cache::builder()
.capacity(config.cache_config.policy_cache_capacity)
.build()
.await
.map_err(|_e| {
DbError::Connection(sea_orm::DbErr::ConnectionAcquire(sea_orm::ConnAcquireErr::Timeout))
})?,
);
let permission_config = Self::load_permission_config(config).await?;
if let Some(ref perm_config) = permission_config {
for (role_name, policy) in &perm_config.roles {
let _ = policy_cache.set(role_name, policy).await;
}
}
Ok((policy_cache, permission_config))
}
#[cfg(feature = "permission")]
async fn load_permission_config(config: &DbConfig) -> DbResult<Option<PermissionConfig>> {
if let Some(ref path) = config.permissions_path {
let content = tokio::fs::read_to_string(path).await.map_err(|e| {
DbError::Config(i18n::t(
"pool-read-config-failed",
&[("path", path.clone()), ("error", e.to_string())],
))
})?;
let perm_config = Self::parse_permission_yaml(&content, path).map_err(|e| {
DbError::Config(i18n::t(
"pool-parse-config-failed",
&[("path", path.clone()), ("error", e.to_string())],
))
})?;
return Ok(Some(perm_config));
}
Ok(None)
}
#[cfg(feature = "permission")]
fn parse_permission_yaml(content: &str, source: &str) -> Result<PermissionConfig, String> {
#[cfg(feature = "yaml")]
{
serde_yaml_ng::from_str(content).map_err(|e| {
i18n::t(
"pool-yaml-parse-error",
&[("source", source.to_string()), ("error", e.to_string())],
)
})
}
#[cfg(not(feature = "yaml"))]
{
let _ = (content, source);
Err("Cannot parse permission config: 'yaml' feature is not enabled".to_string())
}
}
#[cfg(feature = "metrics")]
pub fn metrics(&self) -> Option<&Arc<MetricsCollector>> {
self.inner.metrics_collector.as_ref()
}
pub fn get_actual_config(&self) -> &DbConfig {
&self.inner.config
}
#[cfg(feature = "failover")]
#[allow(dead_code)]
pub(crate) fn advance_to_next_url(&self) {
if let Some(ref config) = self.inner.config.failover_config {
let len = config.urls.len() as u32;
if len <= 1 {
return;
}
self.inner
.current_url_index
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |current| Some((current + 1) % len))
.ok();
}
}
#[cfg(feature = "failover")]
#[allow(dead_code)]
pub(crate) fn current_url(&self) -> &str {
if let Some(ref config) = self.inner.config.failover_config
&& !config.urls.is_empty()
{
let idx = self.inner.current_url_index.load(Ordering::SeqCst) as usize;
return &config.urls[idx.min(config.urls.len() - 1)];
}
&self.inner.config.url
}
#[cfg(not(feature = "failover"))]
#[allow(dead_code)]
pub(crate) fn current_url(&self) -> &str {
&self.inner.config.url
}
#[cfg(feature = "failover")]
#[allow(dead_code)]
pub(crate) async fn validate_connection(&self, conn: &sea_orm::DatabaseConnection) -> bool {
use sea_orm::ConnectionTrait;
let query = self
.inner
.config
.failover_config
.as_ref()
.and_then(|c| c.health_check_query.as_deref())
.unwrap_or("SELECT 1");
conn.execute_unprepared(query).await.is_ok()
}
pub async fn get_session(&self, role: &str) -> DbResult<Session> {
#[cfg(feature = "permission")]
self.validate_role_name(role).await?;
let connection = self.acquire_connection().await?;
let session = Session::new(connection, self.inner.clone(), role.to_string());
Ok(session)
}
#[cfg(feature = "permission")]
async fn validate_role_name(&self, role: &str) -> DbResult<()> {
let permission_config = self.inner.permission_config.load();
if permission_config.is_none() {
let safe_roles = ["admin", "system"];
if !safe_roles.contains(&role) {
return Err(DbError::Permission(format!(
"Role '{}' is not allowed without explicit permission configuration. Allowed roles: {}",
role,
safe_roles.join(", ")
)));
}
return Ok(());
}
if permission_config
.as_ref()
.is_some_and(|c| c.get_role_policy(role).is_none())
{
return Err(DbError::Permission(format!(
"Role '{}' is not defined in permission configuration",
role
)));
}
Ok(())
}
async fn create_connection(config: &DbConfig) -> DbResult<DbConnection> {
let db_type = config.database_type().map_err(|e| {
DbError::Connection(sea_orm::DbErr::Custom(i18n::t(
"pool-invalid-db-url",
&[("error", e.to_string())],
)))
})?;
match db_type {
crate::foundation::DatabaseType::DuckDb => {
#[cfg(feature = "duckdb")]
{
let conn = crate::database::DuckDbConnection::new(&config.url)?;
Ok(DbConnection::DuckDb(conn))
}
#[cfg(not(feature = "duckdb"))]
{
Err(DbError::Connection(sea_orm::DbErr::Custom(
"DuckDB feature is not enabled".to_string(),
)))
}
}
crate::foundation::DatabaseType::Ladybug => {
#[cfg(feature = "ladybug")]
{
let pool_size = config.pool_config.max_connections as usize;
let conn = crate::database::LadybugConnection::new(&config.url, pool_size)?;
Ok(DbConnection::Ladybug(Arc::new(conn)))
}
#[cfg(not(feature = "ladybug"))]
{
Err(DbError::Connection(sea_orm::DbErr::Custom(
"Ladybug feature is not enabled".to_string(),
)))
}
}
crate::foundation::DatabaseType::Neo4j => {
#[cfg(feature = "neo4j")]
{
let (uri, user, password) = crate::database::Neo4jConnection::parse_url(&config.url)?;
let conn = crate::database::Neo4jConnection::new(&uri, &user, &password).await?;
Ok(DbConnection::Neo4j(Arc::new(conn)))
}
#[cfg(not(feature = "neo4j"))]
{
Err(DbError::Connection(sea_orm::DbErr::Custom(
"Neo4j feature is not enabled".to_string(),
)))
}
}
_ => {
let conn = sea_orm::Database::connect(&config.url).await?;
Ok(DbConnection::SeaOrm(conn))
}
}
}
#[cfg(feature = "pool-warmup")]
async fn warmup_connections(&self) -> DbResult<()> {
let initial_connections = self.inner.config.pool_config.min_connections;
let warmup_timeout = Duration::from_secs(self.inner.config.warmup_timeout);
let warmup_retries = self.inner.config.warmup_retries;
let mut connection_tasks = Vec::new();
for _ in 0..initial_connections {
let config = self.inner.config.clone();
connection_tasks.push(async move {
let mut retries = 0;
let mut last_error = None;
while retries <= warmup_retries {
match timeout(warmup_timeout, Self::create_connection(&config)).await {
Ok(Ok(conn)) => return Ok(conn),
Ok(Err(e)) => {
last_error = Some(e);
retries += 1;
if retries <= warmup_retries {
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
Err(_) => {
last_error = Some(DbError::Connection(sea_orm::DbErr::ConnectionAcquire(
sea_orm::ConnAcquireErr::Timeout,
)));
break;
}
}
}
Err(last_error.unwrap_or_else(|| {
DbError::Connection(sea_orm::DbErr::ConnectionAcquire(sea_orm::ConnAcquireErr::Timeout))
}))
});
}
let results = futures::future::join_all(connection_tasks).await;
let mut success_count = 0usize;
let mut errors: Vec<DbError> = Vec::new();
for result in results {
match result {
Ok(conn) => {
self.inner.idle_connections.lock().await.push(conn);
self.inner.total_count.fetch_add(1, Ordering::SeqCst);
success_count += 1;
}
Err(e) => errors.push(e),
}
}
if success_count == 0 && initial_connections > 0 {
return Err(errors.into_iter().next().unwrap_or_else(|| {
DbError::Connection(sea_orm::DbErr::ConnectionAcquire(sea_orm::ConnAcquireErr::Timeout))
}));
}
if !errors.is_empty() {
}
Ok(())
}
pub async fn check_connection_health(&self, conn: &DbConnection) -> bool {
match conn {
DbConnection::SeaOrm(sea_conn) => {
let backend = Self::get_database_backend(&self.inner.config.url);
let result = timeout(
Duration::from_secs(5),
sea_conn.execute_raw(sea_orm::Statement::from_string(backend, "SELECT 1".to_string())),
)
.await;
matches!(result, Ok(Ok(_)))
}
#[cfg(feature = "duckdb")]
DbConnection::DuckDb(duck_conn) => {
let result = timeout(Duration::from_secs(5), duck_conn.health_check()).await;
matches!(result, Ok(Ok(_)))
}
#[cfg(feature = "ladybug")]
DbConnection::Ladybug(conn) => {
let result = timeout(Duration::from_secs(5), conn.health_check()).await;
matches!(result, Ok(Ok(_)))
}
#[cfg(feature = "neo4j")]
DbConnection::Neo4j(conn) => {
let result = timeout(Duration::from_secs(5), conn.health_check()).await;
matches!(result, Ok(Ok(_)))
}
}
}
fn get_database_backend(url: &str) -> sea_orm::DatabaseBackend {
if url.starts_with("sqlite:") {
sea_orm::DatabaseBackend::Sqlite
} else if url.starts_with("postgres:") || url.starts_with("postgresql:") {
sea_orm::DatabaseBackend::Postgres
} else if url.starts_with("mysql:") {
sea_orm::DatabaseBackend::MySql
} else if url.starts_with("duckdb:") {
sea_orm::DatabaseBackend::Sqlite
} else {
sea_orm::DatabaseBackend::Sqlite
}
}
#[cfg(feature = "pool-health-check")]
async fn validate_idle_connections(idle: &mut Vec<DbConnection>, config: &DbConfig) -> (Vec<DbConnection>, usize) {
let backend = Self::get_database_backend(&config.url);
let connections: Vec<DbConnection> = std::mem::take(idle);
let check_futures: Vec<_> = connections
.into_iter()
.map(|conn| async {
let is_valid = match &conn {
DbConnection::SeaOrm(sea_conn) => timeout(
Duration::from_secs(2),
sea_conn.execute_raw(sea_orm::Statement::from_string(backend, "SELECT 1".to_string())),
)
.await
.is_ok_and(|result| result.is_ok()),
#[cfg(feature = "duckdb")]
DbConnection::DuckDb(duck_conn) => timeout(Duration::from_secs(2), duck_conn.health_check())
.await
.is_ok_and(|result| result.is_ok()),
#[cfg(feature = "ladybug")]
DbConnection::Ladybug(graph_conn) => timeout(Duration::from_secs(2), graph_conn.health_check())
.await
.is_ok_and(|result| result.is_ok()),
#[cfg(feature = "neo4j")]
DbConnection::Neo4j(graph_conn) => timeout(Duration::from_secs(2), graph_conn.health_check())
.await
.is_ok_and(|result| result.is_ok()),
};
(conn, is_valid)
})
.collect();
let results: Vec<(DbConnection, bool)> = futures::future::join_all(check_futures).await;
let invalid_count = results.iter().filter(|(_, is_valid)| !*is_valid).count();
let valid_connections: Vec<DbConnection> = results
.into_iter()
.filter_map(|(conn, is_valid)| if is_valid { Some(conn) } else { None })
.collect();
(valid_connections, invalid_count)
}
#[cfg(feature = "pool-health-check")]
pub async fn clean_invalid_connections(&self) -> u32 {
let mut idle = self.inner.idle_connections.lock().await;
let config = &self.inner.config;
let (valid_connections, removed_count) = Self::validate_idle_connections(&mut idle, config).await;
idle.extend(valid_connections);
if removed_count > 0 {
self.inner.total_count.fetch_sub(removed_count as u32, Ordering::SeqCst);
}
removed_count as u32
}
#[cfg(feature = "pool-health-check")]
pub async fn validate_and_recreate_connections(&self) -> Result<u32, sea_orm::DbErr> {
let mut idle = self.inner.idle_connections.lock().await;
let config = &self.inner.config;
let (valid_connections, invalid_count) = Self::validate_idle_connections(&mut idle, config).await;
let mut recreated_count = 0;
if invalid_count > 0 {
self.inner.total_count.fetch_sub(invalid_count as u32, Ordering::SeqCst);
idle.extend(valid_connections);
let current_idle = idle.len();
let needed = config.pool_config.min_connections.saturating_sub(current_idle as u32) as usize;
for _ in 0..needed {
match Self::create_connection(config).await {
Ok(new_conn) => {
idle.push(new_conn);
self.inner.total_count.fetch_add(1, Ordering::SeqCst);
recreated_count += 1;
}
Err(e) => {
return Err(sea_orm::DbErr::Custom(i18n::t(
"pool-recreate-failed",
&[("error", e.to_string())],
)));
}
}
}
} else {
idle.extend(valid_connections);
}
Ok(recreated_count as u32)
}
#[cfg(feature = "pool-health-check")]
pub fn parse_health_check_interval(value: &str) -> u64 {
value.parse::<u64>().ok().map(|v| v.clamp(5, 300)).unwrap_or(30)
}
#[cfg(feature = "pool-health-check")]
fn start_background_health_check(&self) {
let pool = self.clone();
let shutdown = self.inner.health_check_shutdown.clone();
let env_value = std::env::var("DB_HEALTH_CHECK_INTERVAL").unwrap_or_default();
let interval_secs = Self::parse_health_check_interval(&env_value);
tokio::spawn(async move {
let mut interval = interval(Duration::from_secs(interval_secs));
loop {
tokio::select! {
_ = interval.tick() => {
let _ = pool.validate_and_recreate_connections().await;
}
_ = shutdown.notified() => {
break;
}
}
}
});
}
async fn acquire_connection(&self) -> DbResult<DbConnection> {
let waiters = self.inner.wait_count.fetch_add(1, Ordering::SeqCst) + 1;
self.update_max_waiters(waiters);
let timeout_duration = self.inner.config.acquire_timeout_duration();
#[cfg(feature = "metrics")]
let start = Instant::now();
let acquire_result = timeout(timeout_duration, self.inner.connection_semaphore.acquire()).await;
self.inner.wait_count.fetch_sub(1, Ordering::SeqCst);
let permit = match acquire_result {
Ok(Ok(p)) => {
#[cfg(feature = "metrics")]
if let Some(ref collector) = self.inner.metrics_collector {
collector.record_connection_acquire_duration(start.elapsed());
}
p
}
Ok(Err(_)) => {
return Err(DbError::Connection(sea_orm::DbErr::ConnectionAcquire(
sea_orm::ConnAcquireErr::Timeout,
)));
}
Err(_) => {
#[cfg(feature = "metrics")]
{
let elapsed_ms = start.elapsed().as_millis() as u64;
if let Some(ref collector) = self.inner.metrics_collector {
collector.record_connection_timeout_level(elapsed_ms);
}
}
return Err(DbError::Connection(sea_orm::DbErr::ConnectionAcquire(
sea_orm::ConnAcquireErr::Timeout,
)));
}
};
{
let mut idle = self.inner.idle_connections.lock().await;
if let Some(conn) = idle.pop() {
let active = self.inner.active_count.fetch_add(1, Ordering::SeqCst) + 1;
self.update_max_active(active);
self.inner.borrow_count.fetch_add(1, Ordering::SeqCst);
permit.forget();
return Ok(conn);
}
}
self.inner.total_count.fetch_add(1, Ordering::SeqCst);
let active = self.inner.active_count.fetch_add(1, Ordering::SeqCst) + 1;
self.update_max_active(active);
match Self::create_connection(&self.inner.config).await {
Ok(conn) => {
self.inner.borrow_count.fetch_add(1, Ordering::SeqCst);
permit.forget();
Ok(conn)
}
Err(e) => {
self.inner.total_count.fetch_sub(1, Ordering::SeqCst);
self.inner.active_count.fetch_sub(1, Ordering::SeqCst);
drop(permit);
Err(e)
}
}
}
fn update_max_waiters(&self, current_waiters: u32) {
let mut current = self.inner.max_waiters.load(Ordering::Acquire);
while current_waiters > current {
match self
.inner
.max_waiters
.compare_exchange(current, current_waiters, Ordering::SeqCst, Ordering::Acquire)
{
Ok(_) => return,
Err(observed) => {
current = observed;
}
}
}
}
#[cfg(feature = "auto-migrate")]
pub(crate) fn release_connection(&self, conn: DbConnection) {
DbPoolInner::release_connection(&self.inner, conn);
}
pub fn status(&self) -> PoolStatus {
let total = self.inner.total_count.load(Ordering::SeqCst);
let active = self.inner.active_count.load(Ordering::SeqCst);
let wait_count = self.inner.wait_count.load(Ordering::SeqCst);
let max_waiters = self.inner.max_waiters.load(Ordering::SeqCst);
let borrow_count = self.inner.borrow_count.load(Ordering::SeqCst);
let max_active = self.inner.max_active.load(Ordering::SeqCst);
PoolStatus {
total,
active,
idle: total.saturating_sub(active),
wait_count,
max_waiters,
borrow_count,
max_active,
}
}
#[cfg(feature = "metrics")]
pub fn pool_metrics(&self) -> PoolMetrics {
let wait_count = self.inner.wait_count.load(Ordering::SeqCst);
let max_waiters = self.inner.max_waiters.load(Ordering::SeqCst);
if let Some(ref collector) = self.inner.metrics_collector {
let stats = collector.connection_acquire_stats();
PoolMetrics {
slow_acquires: stats.slow_acquires,
timeout_errors: stats.timeout_warn + stats.timeout_error + stats.timeout_critical,
critical_timeouts: stats.timeout_critical,
wait_count,
max_waiters,
}
} else {
PoolMetrics {
slow_acquires: 0,
timeout_errors: 0,
critical_timeouts: 0,
wait_count,
max_waiters,
}
}
}
pub fn config(&self) -> &DbConfig {
&self.inner.config
}
#[cfg(feature = "auto-migrate")]
pub async fn run_auto_migrate(&self) -> Result<u32, DbError> {
if let Some(ref migrations_dir) = self.inner.config.migrations_dir {
self.run_migrations(migrations_dir).await
} else {
Ok(0)
}
}
#[cfg(feature = "auto-migrate")]
pub async fn run_migrations(&self, migrations_dir: &std::path::Path) -> Result<u32, DbError> {
use crate::database::MigrationExecutor;
let db_type = self
.inner
.config
.database_type()
.map_err(|e| DbError::Config(e.to_string()))?;
let connection = self.acquire_connection().await?;
let connection_for_migration = connection.as_sea_orm()?.clone();
let mut executor = MigrationExecutor::new(connection_for_migration, db_type);
let applied = executor.run_migrations(migrations_dir).await?;
self.release_connection(connection);
Ok(applied)
}
}
impl Drop for DbPool {
fn drop(&mut self) {
self.inner.health_check_shutdown.notify_one();
}
}
#[cfg(feature = "metrics")]
#[derive(Debug, Clone)]
pub struct PoolMetrics {
pub slow_acquires: u64,
pub timeout_errors: u64,
pub critical_timeouts: u64,
pub wait_count: u32,
pub max_waiters: u32,
}
#[derive(Debug, Clone)]
pub struct PoolStatus {
pub total: u32,
pub active: u32,
pub idle: u32,
pub wait_count: u32,
pub max_waiters: u32,
pub borrow_count: u64,
pub max_active: u32,
}
#[async_trait]
impl super::ConnectionPool for DbPool {
async fn get_session(&self, role: &str) -> DbResult<Session> {
self.get_session(role).await
}
fn status(&self) -> PoolStatus {
self.status()
}
fn config(&self) -> &DbConfig {
self.config()
}
}
#[cfg(test)]
mod tests {
#![allow(unused_imports)]
use super::*;
use crate::foundation::PoolConfig;
#[cfg(feature = "ladybug")]
#[test]
fn test_ladybug_connection_is_graph() {
let conn = DbConnection::Ladybug(Arc::new(
crate::database::LadybugConnection::new(":memory:", 1).expect("Failed to create LadybugConnection"),
));
assert!(conn.is_graph(), "Ladybug connection should be graph");
assert!(!conn.is_duckdb(), "Ladybug connection should not be duckdb");
}
#[cfg(feature = "ladybug")]
#[test]
fn test_ladybug_connection_as_graph_returns_ok() {
let conn = DbConnection::Ladybug(Arc::new(
crate::database::LadybugConnection::new(":memory:", 1).expect("Failed to create LadybugConnection"),
));
let result = conn.as_graph();
assert!(result.is_ok(), "as_graph() on Ladybug should return Ok");
let graph = result.unwrap();
assert_eq!(graph.backend_name(), "ladybug");
}
#[cfg(feature = "ladybug")]
#[test]
fn test_ladybug_connection_as_sea_orm_returns_err() {
let conn = DbConnection::Ladybug(Arc::new(
crate::database::LadybugConnection::new(":memory:", 1).expect("Failed to create LadybugConnection"),
));
let result = conn.as_sea_orm();
assert!(result.is_err(), "as_sea_orm() on Ladybug should return Err");
}
#[cfg(feature = "ladybug")]
#[tokio::test]
async fn test_create_connection_ladybug_memory() {
let config = DbConfig {
url: "ladybug::memory:".to_string(),
pool_config: PoolConfig {
max_connections: 4,
..Default::default()
},
..Default::default()
};
let conn = DbPool::create_connection(&config)
.await
.expect("create_connection for ladybug::memory: should succeed");
assert!(conn.is_graph(), "should be graph connection");
let graph = conn.as_graph().expect("as_graph should succeed");
assert_eq!(graph.backend_name(), "ladybug");
}
#[cfg(feature = "ladybug")]
#[tokio::test]
async fn test_create_connection_ladybug_health_check() {
let config = DbConfig {
url: "ladybug::memory:".to_string(),
pool_config: PoolConfig {
max_connections: 2,
..Default::default()
},
..Default::default()
};
let conn = DbPool::create_connection(&config)
.await
.expect("create_connection for ladybug::memory: should succeed");
let graph = conn.as_graph().expect("as_graph should succeed");
graph.health_check().await.expect("health_check should pass");
}
#[cfg(feature = "neo4j")]
#[test]
fn test_neo4j_connection_is_graph() {
let conn = DbConnection::Neo4j(Arc::new(crate::database::Neo4jConnection::new_placeholder()));
assert!(conn.is_graph(), "Neo4j connection should be graph");
assert!(!conn.is_duckdb(), "Neo4j connection should not be duckdb");
}
#[cfg(feature = "neo4j")]
#[test]
fn test_neo4j_connection_as_graph_returns_ok() {
let conn = DbConnection::Neo4j(Arc::new(crate::database::Neo4jConnection::new_placeholder()));
let result = conn.as_graph();
assert!(result.is_ok(), "as_graph() on Neo4j should return Ok");
let graph = result.unwrap();
assert_eq!(graph.backend_name(), "neo4j");
}
#[cfg(feature = "neo4j")]
#[test]
fn test_neo4j_connection_as_sea_orm_returns_err() {
let conn = DbConnection::Neo4j(Arc::new(crate::database::Neo4jConnection::new_placeholder()));
let result = conn.as_sea_orm();
assert!(result.is_err(), "as_sea_orm() on Neo4j should return Err");
}
#[cfg(feature = "neo4j")]
#[tokio::test]
#[ignore = "需要 Neo4j 服务器,设置 NEO4J_URL/NEO4J_USER/NEO4J_PASSWORD 环境变量后运行"]
async fn test_create_connection_neo4j() {
let url = std::env::var("NEO4J_URL").unwrap_or_else(|_| "neo4j://localhost:7687".to_string());
let config = DbConfig {
url,
pool_config: PoolConfig {
max_connections: 4,
..Default::default()
},
..Default::default()
};
let conn = DbPool::create_connection(&config)
.await
.expect("create_connection for neo4j should succeed");
assert!(conn.is_graph(), "should be graph connection");
let graph = conn.as_graph().expect("as_graph should succeed");
assert_eq!(graph.backend_name(), "neo4j");
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_sea_orm_connection_is_graph_returns_false() {
let sea_conn = sea_orm::Database::connect("sqlite::memory:")
.await
.expect("sqlite memory connection");
let conn = DbConnection::SeaOrm(sea_conn);
assert!(!conn.is_graph(), "SeaOrm connection should not be graph");
assert!(!conn.is_duckdb(), "SeaOrm connection should not be duckdb");
}
#[cfg(all(feature = "sqlite", any(feature = "ladybug", feature = "neo4j")))]
#[tokio::test]
async fn test_sea_orm_connection_as_graph_returns_err() {
let sea_conn = sea_orm::Database::connect("sqlite::memory:")
.await
.expect("sqlite memory connection");
let conn = DbConnection::SeaOrm(sea_conn);
let result = conn.as_graph();
assert!(result.is_err(), "as_graph() on SeaOrm should return Err");
}
#[test]
fn test_db_connection_debug_format() {
#[cfg(feature = "ladybug")]
{
let conn = DbConnection::Ladybug(Arc::new(
crate::database::LadybugConnection::new(":memory:", 1).expect("Failed to create LadybugConnection"),
));
let debug_str = format!("{conn:?}");
assert!(
debug_str.contains("Ladybug"),
"Debug should contain 'Ladybug': {debug_str}"
);
}
#[cfg(feature = "neo4j")]
{
let conn = DbConnection::Neo4j(Arc::new(crate::database::Neo4jConnection::new_placeholder()));
let debug_str = format!("{conn:?}");
assert!(debug_str.contains("Neo4j"), "Debug should contain 'Neo4j': {debug_str}");
}
}
#[test]
fn test_get_database_backend_sqlite() {
assert!(matches!(
DbPool::get_database_backend("sqlite::memory:"),
sea_orm::DatabaseBackend::Sqlite
));
assert!(matches!(
DbPool::get_database_backend("sqlite:test.db"),
sea_orm::DatabaseBackend::Sqlite
));
}
#[test]
fn test_get_database_backend_postgres() {
assert!(matches!(
DbPool::get_database_backend("postgres://localhost/db"),
sea_orm::DatabaseBackend::Postgres
));
assert!(matches!(
DbPool::get_database_backend("postgresql://localhost/db"),
sea_orm::DatabaseBackend::Postgres
));
}
#[test]
fn test_get_database_backend_mysql() {
assert!(matches!(
DbPool::get_database_backend("mysql://localhost/db"),
sea_orm::DatabaseBackend::MySql
));
}
#[test]
fn test_get_database_backend_duckdb_fallback() {
assert!(matches!(
DbPool::get_database_backend("duckdb::memory:"),
sea_orm::DatabaseBackend::Sqlite
));
}
#[test]
fn test_get_database_backend_unknown_fallback() {
assert!(matches!(
DbPool::get_database_backend("unknown://something"),
sea_orm::DatabaseBackend::Sqlite
));
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_db_connection_is_duckdb_without_feature() {
let conn = DbConnection::SeaOrm(sea_orm::Database::connect("sqlite::memory:").await.unwrap());
assert!(!conn.is_duckdb());
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_db_connection_is_graph_seaorm() {
let conn = DbConnection::SeaOrm(sea_orm::Database::connect("sqlite::memory:").await.unwrap());
assert!(!conn.is_graph());
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_db_connection_as_sea_orm_success() {
let sea_conn = sea_orm::Database::connect("sqlite::memory:")
.await
.expect("sqlite memory connection");
let conn = DbConnection::SeaOrm(sea_conn);
assert!(conn.as_sea_orm().is_ok());
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_pool_status_and_config() {
let config = DbConfig {
url: "sqlite::memory:".to_string(),
pool_config: PoolConfig {
max_connections: 10,
min_connections: 2,
..Default::default()
},
..Default::default()
};
let pool = DbPool::with_config(config).await.expect("should create pool");
let status = pool.status();
assert_eq!(status.total, 2); assert_eq!(status.idle, 2);
assert_eq!(status.borrow_count, 0);
assert_eq!(pool.config().url, "sqlite::memory:");
assert_eq!(pool.config().pool_config.max_connections, 10);
assert_eq!(pool.config().pool_config.min_connections, 2);
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_pool_update_max_active() {
let config = DbConfig {
url: "sqlite::memory:".to_string(),
..Default::default()
};
let pool = DbPool::with_config(config).await.expect("should create pool");
assert_eq!(pool.inner.max_active.load(Ordering::SeqCst), 0);
pool.update_max_active(5);
assert_eq!(pool.inner.max_active.load(Ordering::SeqCst), 5);
pool.update_max_active(3);
assert_eq!(pool.inner.max_active.load(Ordering::SeqCst), 5);
pool.update_max_active(10);
assert_eq!(pool.inner.max_active.load(Ordering::SeqCst), 10);
}
#[cfg(not(feature = "duckdb"))]
#[tokio::test]
async fn test_create_connection_duckdb_not_enabled() {
let config = DbConfig {
url: "duckdb::memory:".to_string(),
..Default::default()
};
let result = DbPool::create_connection(&config).await;
assert!(result.is_err(), "DuckDB connection should fail without duckdb feature");
}
#[tokio::test]
async fn test_create_connection_ladybug_not_enabled() {
let config = DbConfig {
url: "ladybug::memory:".to_string(),
..Default::default()
};
let result = DbPool::create_connection(&config).await;
assert!(
result.is_err(),
"Ladybug connection should fail without ladybug feature"
);
}
#[tokio::test]
async fn test_create_connection_neo4j_not_enabled() {
let config = DbConfig {
url: "neo4j://localhost:7687".to_string(),
..Default::default()
};
let result = DbPool::create_connection(&config).await;
assert!(result.is_err(), "Neo4j connection should fail without neo4j feature");
}
#[cfg(feature = "sqlite")]
#[test]
fn test_pool_try_from_with_permission_returns_error() {
let config = DbConfig::default();
let result = DbPool::try_from(&config);
assert!(result.is_err(), "try_from should fail with permission feature enabled");
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_pool_current_url() {
let config = DbConfig {
url: "sqlite::memory:".to_string(),
..Default::default()
};
let pool = DbPool::with_config(config).await.expect("should create pool");
assert_eq!(pool.current_url(), "sqlite::memory:");
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_seaorm_debug_format() {
let sea_conn = sea_orm::Database::connect("sqlite::memory:")
.await
.expect("sqlite memory connection");
let conn = DbConnection::SeaOrm(sea_conn);
let debug_str = format!("{conn:?}");
assert!(
debug_str.contains("SeaOrm"),
"Debug should contain 'SeaOrm': {debug_str}"
);
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_connection_pool_trait_methods() {
use super::super::ConnectionPool;
let config = DbConfig {
url: "sqlite::memory:".to_string(),
pool_config: PoolConfig {
max_connections: 5,
..Default::default()
},
..Default::default()
};
let pool = DbPool::with_config(config).await.expect("should create pool");
let status = ConnectionPool::status(&pool);
assert_eq!(status.total, 5);
let cfg = ConnectionPool::config(&pool);
assert_eq!(cfg.url, "sqlite::memory:");
assert_eq!(cfg.pool_config.max_connections, 5);
let session = ConnectionPool::get_session(&pool, "admin").await;
assert!(
session.is_ok(),
"get_session should succeed for admin: {:?}",
session.err()
);
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_check_connection_health_sqlite() {
let config = DbConfig {
url: "sqlite::memory:".to_string(),
..Default::default()
};
let pool = DbPool::with_config(config).await.expect("should create pool");
let sea_conn = sea_orm::Database::connect("sqlite::memory:")
.await
.expect("sqlite memory connection");
let conn = DbConnection::SeaOrm(sea_conn);
let healthy = pool.check_connection_health(&conn).await;
assert!(healthy, "SQLite memory connection should be healthy");
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_release_connection_pool_full() {
let config = DbConfig {
url: "sqlite::memory:".to_string(),
pool_config: PoolConfig {
max_connections: 2,
min_connections: 1,
..Default::default()
},
..Default::default()
};
let pool = DbPool::with_config(config).await.expect("should create pool");
let session1 = pool.get_session("admin").await.expect("session 1");
let session2 = pool.get_session("admin").await.expect("session 2");
drop(session1);
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
drop(session2);
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
let status = pool.status();
assert!(status.idle <= 2, "idle should be <= max_connections: {}", status.idle);
}
#[cfg(any(feature = "cache", feature = "oxcache-integration"))]
#[tokio::test]
async fn test_pool_set_and_get_cache_provider() {
use crate::foundation::DbError;
use std::future::Future;
use std::pin::Pin;
struct NoopCacheProvider;
impl crate::domain::DbCacheProvider for NoopCacheProvider {
fn get<'a>(
&'a self,
_key: &'a str,
) -> Pin<Box<dyn Future<Output = Result<Option<Vec<u8>>, DbError>> + Send + 'a>> {
Box::pin(async { Ok(None) })
}
fn set<'a>(
&'a self,
_key: &'a str,
_value: Vec<u8>,
_ttl: Option<std::time::Duration>,
) -> Pin<Box<dyn Future<Output = Result<(), DbError>> + Send + 'a>> {
Box::pin(async { Ok(()) })
}
fn delete<'a>(&'a self, _key: &'a str) -> Pin<Box<dyn Future<Output = Result<(), DbError>> + Send + 'a>> {
Box::pin(async { Ok(()) })
}
}
let config = DbConfig {
url: "sqlite::memory:".to_string(),
..Default::default()
};
let mut pool = DbPool::with_config(config).await.expect("should create pool");
assert!(pool.cache_provider().is_none());
let provider = Arc::new(NoopCacheProvider);
pool.set_cache_provider(provider);
assert!(pool.cache_provider().is_some());
}
#[cfg(feature = "permission")]
#[tokio::test]
async fn test_validate_role_name_with_config_unknown_role() {
use std::io::Write;
let yaml_content = r#"
roles:
admin:
tables:
- name: "*"
operations: ["select", "insert", "update", "delete"]
"#;
let tmp_dir = std::env::temp_dir();
let yaml_path = tmp_dir.join("test_perm_config.yaml");
{
let mut file = std::fs::File::create(&yaml_path).expect("create temp file");
file.write_all(yaml_content.as_bytes()).expect("write temp file");
}
let config = DbConfig {
url: "sqlite::memory:".to_string(),
permissions_path: Some(yaml_path.to_string_lossy().to_string()),
..Default::default()
};
let pool = DbPool::with_config(config).await.expect("should create pool");
let result = pool.get_session("admin").await;
assert!(result.is_ok(), "admin should be allowed: {:?}", result.err());
let result = pool.get_session("unknown_role").await;
assert!(result.is_err(), "unknown_role should be rejected");
match result.err().unwrap() {
DbError::Permission(msg) => {
assert!(
msg.contains("not defined in permission configuration"),
"error should mention role not defined: {}",
msg
);
}
other => panic!("expected DbError::Permission, got {:?}", other),
}
let _ = std::fs::remove_file(&yaml_path);
}
#[cfg(feature = "permission")]
#[tokio::test]
async fn test_validate_role_name_no_config_unsafe_role() {
let config = DbConfig {
url: "sqlite::memory:".to_string(),
..Default::default()
};
let pool = DbPool::with_config(config).await.expect("should create pool");
let result = pool.get_session("admin").await;
assert!(result.is_ok(), "admin should be allowed: {:?}", result.err());
let result = pool.get_session("system").await;
assert!(result.is_ok(), "system should be allowed: {:?}", result.err());
let result = pool.get_session("hacker").await;
assert!(result.is_err(), "hacker should be rejected");
}
}