orion-server 1.0.0

Turn business logic into live REST/Kafka services. Declare workflows as JSON and Orion runs them, with rate limiting, circuit breakers, versioning, and observability built in
//! Dynamic SQL connection pool cache for external database connectors.
//!
//! Lazily creates and caches [`sqlx::AnyPool`] connections keyed by connector
//! name. The pool backend (Postgres, MySQL, SQLite) is selected at runtime
//! based on the connection-string URL scheme.
//!
//! **Important:** [`sqlx::any::install_default_drivers()`] must be called once
//! at application startup before any pool is created.

use std::time::Duration;

use sqlx::{AnyPool, any::AnyPoolOptions};

use super::lru_cache::LruCache;
use crate::connector::DbConnectorConfig;
use crate::errors::OrionError;

/// Lazily creates and caches SQL connection pools keyed by connector name.
/// Bounded by `max_entries` with LRU eviction.
pub struct SqlPoolCache {
    cache: LruCache<AnyPool>,
}

impl SqlPoolCache {
    pub fn new(max_entries: usize) -> Self {
        Self {
            // F17: close evicted pools on a detached task — new acquires on
            // the closed pool fail fast, in-flight queries finish, and the
            // TCP connections are returned instead of counting against the
            // remote DB's max_connections until the last Arc drops.
            cache: LruCache::with_evict_handler(max_entries, "sql_pool", |pool: AnyPool| {
                tokio::spawn(async move { pool.close().await });
            }),
        }
    }

    /// Get or lazily create a pool for the named connector.
    pub async fn get_pool(
        &self,
        connector_name: &str,
        config: &DbConnectorConfig,
    ) -> Result<AnyPool, OrionError> {
        let conn_str = config.connection_string.clone();
        let max_conns = config.max_connections.unwrap_or(5);
        let connect_timeout = config.connect_timeout_ms.unwrap_or(5000);

        self.cache
            .get_or_create(connector_name, || async move {
                // S6: refuse a private/internal target before dialling. Only on
                // the create path — a cached pool was checked when it was
                // opened, and re-resolving per query would put a DNS round trip
                // on the hot path.
                crate::validation::check_db_endpoint(connector_name, config).await?;

                AnyPoolOptions::new()
                    .max_connections(max_conns)
                    .acquire_timeout(Duration::from_millis(connect_timeout))
                    .connect(&conn_str)
                    .await
                    .map_err(|e| OrionError::Internal {
                        context: format!("Failed to connect to external DB '{connector_name}'"),
                        source: Some(Box::new(e)),
                    })
            })
            .await
    }

    /// Evict a cached pool (e.g., when connector config changes).
    pub async fn evict(&self, connector_name: &str) {
        self.cache.evict(connector_name).await;
    }

    pub async fn evict_all(&self) {
        self.cache.evict_all().await;
    }
}

impl Default for SqlPoolCache {
    fn default() -> Self {
        Self::new(100)
    }
}