use std::time::Duration;
#[derive(Debug, Clone)]
pub struct BrowserPoolConfig {
pub max_pool_size: usize,
pub warmup_count: usize,
pub ping_interval: Duration,
pub browser_ttl: Duration,
pub max_ping_failures: u32,
pub warmup_timeout: Duration,
pub warmup_stagger: Duration,
}
impl Default for BrowserPoolConfig {
fn default() -> Self {
Self {
max_pool_size: 5,
warmup_count: 3,
ping_interval: Duration::from_secs(15),
browser_ttl: Duration::from_secs(3600), max_ping_failures: 3,
warmup_timeout: Duration::from_secs(60),
warmup_stagger: Duration::from_secs(30),
}
}
}
pub struct BrowserPoolConfigBuilder {
config: BrowserPoolConfig,
}
impl BrowserPoolConfigBuilder {
pub fn new() -> Self {
Self {
config: BrowserPoolConfig::default(),
}
}
pub fn max_pool_size(mut self, size: usize) -> Self {
self.config.max_pool_size = size;
self
}
pub fn warmup_count(mut self, count: usize) -> Self {
self.config.warmup_count = count;
self
}
pub fn ping_interval(mut self, interval: Duration) -> Self {
self.config.ping_interval = interval;
self
}
pub fn browser_ttl(mut self, ttl: Duration) -> Self {
self.config.browser_ttl = ttl;
self
}
pub fn max_ping_failures(mut self, failures: u32) -> Self {
self.config.max_ping_failures = failures;
self
}
pub fn warmup_timeout(mut self, timeout: Duration) -> Self {
self.config.warmup_timeout = timeout;
self
}
pub fn warmup_stagger(mut self, stagger: Duration) -> Self {
self.config.warmup_stagger = stagger;
self
}
pub fn build(self) -> std::result::Result<BrowserPoolConfig, String> {
if self.config.max_pool_size == 0 {
return Err("max_pool_size must be greater than 0".to_string());
}
if self.config.warmup_count > self.config.max_pool_size {
return Err("warmup_count cannot exceed max_pool_size".to_string());
}
Ok(self.config)
}
}
impl Default for BrowserPoolConfigBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "env-config")]
pub mod env {
use super::*;
use crate::error::BrowserPoolError;
pub const ENV_FILE_NAME: &str = "app.env";
pub fn load_env_file() -> Result<std::path::PathBuf, dotenvy::Error> {
dotenvy::from_filename(ENV_FILE_NAME)
}
pub fn from_env() -> Result<BrowserPoolConfig, BrowserPoolError> {
match load_env_file() {
Ok(path) => {
log::info!("⚙️ Loaded configuration from: {:?}", path);
}
Err(e) => {
log::debug!(
"⚠️ No {} file found or failed to load: {} (using environment variables and defaults)",
ENV_FILE_NAME,
e
);
}
}
let max_pool_size = std::env::var("BROWSER_POOL_SIZE")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(5);
let warmup_count = std::env::var("BROWSER_WARMUP_COUNT")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(3);
let ttl_seconds = std::env::var("BROWSER_TTL_SECONDS")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(3600u64);
let warmup_timeout_seconds = std::env::var("BROWSER_WARMUP_TIMEOUT_SECONDS")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(60u64);
let ping_interval_seconds = std::env::var("BROWSER_PING_INTERVAL_SECONDS")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(15u64);
let max_ping_failures = std::env::var("BROWSER_MAX_PING_FAILURES")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(3);
log::info!("' Loading pool configuration from environment:");
log::info!(" - Max pool size: {}", max_pool_size);
log::info!(" - Warmup count: {}", warmup_count);
log::info!(
" - Browser TTL: {}s ({}min)",
ttl_seconds,
ttl_seconds / 60
);
log::info!(" - Warmup timeout: {}s", warmup_timeout_seconds);
log::info!(" - Ping interval: {}s", ping_interval_seconds);
log::info!(" - Max ping failures: {}", max_ping_failures);
BrowserPoolConfigBuilder::new()
.max_pool_size(max_pool_size)
.warmup_count(warmup_count)
.browser_ttl(Duration::from_secs(ttl_seconds))
.warmup_timeout(Duration::from_secs(warmup_timeout_seconds))
.ping_interval(Duration::from_secs(ping_interval_seconds))
.max_ping_failures(max_ping_failures)
.build()
.map_err(BrowserPoolError::Configuration)
}
pub fn chrome_path_from_env() -> Option<String> {
std::env::var("CHROME_PATH").ok()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_builder() {
let config = BrowserPoolConfigBuilder::new()
.max_pool_size(10)
.warmup_count(5)
.browser_ttl(Duration::from_secs(7200))
.warmup_timeout(Duration::from_secs(120))
.build()
.unwrap();
assert_eq!(config.max_pool_size, 10);
assert_eq!(config.warmup_count, 5);
assert_eq!(config.browser_ttl.as_secs(), 7200);
assert_eq!(config.warmup_timeout.as_secs(), 120);
}
#[test]
fn test_config_validation() {
let result = BrowserPoolConfigBuilder::new().max_pool_size(0).build();
assert!(result.is_err());
let err_msg = result.unwrap_err();
assert!(
err_msg.contains("max_pool_size must be greater than 0"),
"Expected validation error message, got: {}",
err_msg
);
}
#[test]
fn test_config_warmup_exceeds_pool() {
let result = BrowserPoolConfigBuilder::new()
.max_pool_size(5)
.warmup_count(10)
.build();
assert!(result.is_err());
let err_msg = result.unwrap_err();
assert!(
err_msg.contains("warmup_count cannot exceed max_pool_size"),
"Expected validation error message, got: {}",
err_msg
);
}
#[test]
fn test_config_defaults() {
let config = BrowserPoolConfig::default();
assert_eq!(config.max_pool_size, 5, "Default pool size should be 5");
assert_eq!(config.warmup_count, 3, "Default warmup should be 3");
assert_eq!(
config.ping_interval,
Duration::from_secs(15),
"Default ping interval should be 15s"
);
assert_eq!(
config.browser_ttl,
Duration::from_secs(3600),
"Default TTL should be 1 hour"
);
assert_eq!(
config.max_ping_failures, 3,
"Default max failures should be 3"
);
assert_eq!(
config.warmup_timeout,
Duration::from_secs(60),
"Default warmup timeout should be 60s"
);
}
#[test]
fn test_config_builder_chaining() {
let config = BrowserPoolConfigBuilder::new()
.max_pool_size(8)
.warmup_count(4)
.ping_interval(Duration::from_secs(30))
.browser_ttl(Duration::from_secs(1800))
.max_ping_failures(5)
.warmup_timeout(Duration::from_secs(90))
.build()
.unwrap();
assert_eq!(config.max_pool_size, 8);
assert_eq!(config.warmup_count, 4);
assert_eq!(config.ping_interval.as_secs(), 30);
assert_eq!(config.browser_ttl.as_secs(), 1800);
assert_eq!(config.max_ping_failures, 5);
assert_eq!(config.warmup_timeout.as_secs(), 90);
}
#[test]
fn test_builder_default() {
let builder: BrowserPoolConfigBuilder = Default::default();
let config = builder.build().unwrap();
assert_eq!(config.max_pool_size, 5);
assert_eq!(config.warmup_count, 3);
}
}